基于springboot和redis实现的抢红包demo

本文基于springboot和redis实现了抢红包的基本功能,代码请见:https://github.com/futao1991/redPacket_demo

一、基本实现步骤

        redis中维护3中类型的键,分别为redPacket_num,类型为Hash,记录每个红包的数量;redPacket_record类型为Hash,记录每个用户的抢红包记录,以防止一个用户对一个红包进行重复抢;用户发红包之后,在redis中生成一个红包uuid的键,其值为红包金额,其它用户抢红包时,对该红包uuid的键进行操作。

        在抢红包的过程中,需要同时对3种键进行操作,为了保证操作的原子性,抢红包的业务操作放到lua脚本中完成,脚本如下:

local uuid = string.format("redPacket-%s", KEYS[2])
local record_id = string.format("%s-%s", KEYS[1], KEYS[2])
if (redis.call('EXISTS', uuid) ~= 1) then
   return -2 -- not exists
end

if (redis.call('HEXISTS', 'redPacket_record', record_id) == 1) then
   return -1 -- has get red packet
end

local total_money = redis.call('GET', uuid);
local expire_time = redis.call('TTL', uuid);
local total_num = redis.call('HGET', 'redPacket_num', KEYS[2]);
if tonumber(total_num) > 0 then
   if (tonumber(total_num) == 1) then
      redis.call('HSET', 'redPacket_record', record_id, total_money)
      redis.call('SET', uuid, '0')
      redis.call('EXPIRE', uuid, expire_time)
      redis.call('HSET', 'redPacket_num', KEYS[2], '0')
      return total_money + 0
   else
      local money = math.random(1, total_money - 1)
      redis.call('HSET', 'redPacket_record', record_id, money)
      redis.call('SET', uuid, total_money - money)
      redis.call('EXPIRE', uuid, expire_time)
      redis.call('HSET', 'redPacket_num', KEYS[2], total_num - 1)
      return money
   end
else
   return 0
end

         以上的逻辑中,首先判断红包的个数,如果个数为1,则直接返回该红包的金额;如果大于1,则取一个随机值,同时将红包的金额减去该随机值,红包数量减一;红包金额键的命令方式为redPacket-uuid,由于发出的红包在一定时间内未被领取会过期,因此在创建该键的时候需要指定过期时间,当更新该键时,需要将当期剩余的过期时间一并写入进去。

二、抢红包的业务逻辑

       数据库中维护了3张表,分别为: account表记录用户信息,包括用户的账号金额,用于红包扣减和到账;record表用于记录红包发和抢的事件;red_packet表记录某个红包的信息,包括该红包的金额和数量。

       当用户抢红包时,返回的逻辑可能包含以下几种情况:
       1) 抢到了红包

       2) 未抢到红包,即返回的红包金额为0

       3) 已经抢过该红包,不能再抢

       4) 该红包已过期或不存在

       当通过lua脚本完成抢红包的逻辑之后,需要把更新数据表的业务信息,包括增加账户金额,记录抢红包的事件,更新红包信息等,在高并发场景中,有时希望将抢红包的过程与更新业务逻辑剥离开来,因此当通过lua脚本抢到红包之后,可能采取消息队列的通知机制来完成业务操作,在demo中为了简化设计,消息队列机制采用内存中消息队列Deque来通知。

三、红包过期操作

       在实际的业务中,当发出的红包在规定时间内未被全部抢完后,剩余的红包会退还到发送人的账户中。这里我们基于redis的过期监听机制来完成,在创建红包uuid的键时指定了过期时间,当过期时间到时,通过监听获取到过期的红包uuid:

       首先在redis的配置文件中打开过期监听配置:

notify-keyspace-events Ex

       springboot中提供了现成的监听类KeyExpirationEventMessageListener:

@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {

    private static final Logger logger = LoggerFactory.getLogger("console");

    @Autowired
    private RedPacketMapper redPacketMapper;

    @Autowired
    private AbstractMsgQueueService queueService;

    public RedisKeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        logger.info("key {} expired!", expiredKey);
    }
}

          在onMessage方法中可以获取到过期的key,再根据key进行业务操作即可,注意这里的监听机制仅能获取到过期的key,即红包的uuid,无法获取到对应的value,也即红包金额,因此我们需要在数据库中维护一个红包记录表,以记录红包的金额,返回剩余红包金额时,根据uuid从数据表中查询。

四、效果演示

       项目启动之后,我们模仿用户发出一个红包,金额为10元,数量为5共,另外有9个用户同时抢红包:

       基于springboot和redis实现的抢红包demo_第1张图片

       从打印的日志可以看出,9个用户中共有5个用户抢到了红包,其余4个用户提示红包已抢完。

 

       再模拟红包为抢完过期退还的情况,让一个用户发出红包,金额为10元,数量为5个,过期时间为1分钟;在该1分钟内有3个用户抢红包,时间到期之后,剩余的红包金额退还用户账户:

       基于springboot和redis实现的抢红包demo_第2张图片

        从日志可以看出,一分钟过后,红包剩余的金额已返回到用户账户。

你可能感兴趣的:(基于springboot和redis实现的抢红包demo)