基于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个用户同时抢红包:

       

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

 

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

       

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

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是使用 Spring Boot 基于 Redis 实现分布式锁的示例代码: 首先在 pom.xml 中添加 Redis 相关依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 然后创建一个 Redis 分布式锁的工具类: ```java @Component public class RedisLockUtil { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 尝试获取分布式锁 * @param key 锁的名称 * @param value 锁的值 * @param expireTime 锁的过期时间,单位为秒 * @return 是否成功获取锁 */ public boolean tryLock(String key, String value, long expireTime) { Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTime)); return result != null ? result : false; } /** * 释放分布式锁 * @param key 锁的名称 * @param value 锁的值 */ public void unlock(String key, String value) { String currentValue = redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(currentValue) && StringUtils.equals(currentValue, value)) { redisTemplate.delete(key); } } } ``` 在上述代码中,我们使用了 RedisTemplate 对 Redis 进行操作,其中 tryLock() 方法尝试获取分布式锁,如果获取成功则返回 true,否则返回 false。unlock() 方法释放分布式锁。 最后,在需要使用分布式锁的地方注入 RedisLockUtil 并使用它进行加锁和解锁,示例如下: ```java @Service public class UserService { @Autowired private RedisLockUtil redisLockUtil; public void updateUser(User user) { String lockKey = "updateUser:" + user.getId(); String lockValue = UUID.randomUUID().toString(); try { boolean lockResult = redisLockUtil.tryLock(lockKey, lockValue, 60); if (lockResult) { // 获取分布式锁成功,执行更新操作 // ... } else { // 获取分布式锁失败,抛出异常或者重试 // ... } } finally { // 释放分布式锁 redisLockUtil.unlock(lockKey, lockValue); } } } ``` 在上述代码中,我们调用 RedisLockUtil 的 tryLock() 方法尝试获取分布式锁,如果获取成功则执行更新操作,否则抛出异常或者重试。最后在 finally 块中调用 RedisLockUtil 的 unlock() 方法释放分布式锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值