本文基于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个用户同时抢红包:
从打印的日志可以看出,9个用户中共有5个用户抢到了红包,其余4个用户提示红包已抢完。
再模拟红包为抢完过期退还的情况,让一个用户发出红包,金额为10元,数量为5个,过期时间为1分钟;在该1分钟内有3个用户抢红包,时间到期之后,剩余的红包金额退还用户账户:
从日志可以看出,一分钟过后,红包剩余的金额已返回到用户账户。