使用redis实现红包功能
红包实现
结合本人多次红包开发经验。开发红包首先你要了解何为并发,为什么会产生并发,这几个问题。并发简单来说因为多个线程同时对存储在同一个地方的同一个数据进行修改,然后更新。本来1+1=2,但是因为并发得到的答案是1+1=1的问题。
主要使用redis实现,首先要了解到红包的难点有以下几点:
- 单个用户多次操作并发性(检查用户是否已经抢过)。
- 多个用户操作并发性(检查红包是否已经抢完)。
实现思路:将红包总个数存入字符串格式 (red_id:总个数),在领取的时候使用redis的set集合不可重复特性判断用户是否已经被领取过。如果add进去返回为null,则表示已经领取过了(添加失败),否则添加成功,再获取set集合的长度,表示已经领取了多少个,再获取红包总个数进行判断是否已经抢完。这三条redis命令放在事务里面,保证整个操作是原子性,后续数据库更新操作不要出现getandset操作。
发红包 (redis事务+set集合)
//缓存初始化数据
//键的过期时间为过期时间+5秒(防止键值过期时间过快)
long expireTime = date - System.currentTimeMillis() + 5000;
RedisTemplate<String, Object> redisTemplate = redisUtils.getRedisTemplate();
//添加事务,保持两个键过期时间一致
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
//添加红包总数到缓存中
redisTemplate.opsForValue().set(CacheKey.QRCODE_FLOAT_KEY_TOTAL + redId, String.valueOf(10));
//初始化set集合(存领取用户记录,初始化的时候会存入一个空值,所以后续获取领取个数的时候需要减1)
redisTemplate.opsForSet().add(CacheKey.QRCODE_KEY_UNIQUE + redId, "");
redisTemplate.expire(CacheKey.QRCODE_KEY_UNIQUE + redId, expireTime, TimeUnit.MILLISECONDS);
redisTemplate.expire(CacheKey.QRCODE_FLOAT_KEY_TOTAL + redId, expireTime, TimeUnit.MILLISECONDS);
redisTemplate.exec();
redisTemplate.setEnableTransactionSupport(false);
抢红包 (redis事务+set集合)
private ReturnModel putQRcodeGoldMoreMuti(ReturnModel returnModel, String uid, String content, Date endTime, RedisTemplate<String, Object> redisTemplate){
//已领取的人数
Long receive = redisTemplate.opsForSet().size(CacheKey.QRCODE_KEY_UNIQUE + content);
//红包总数
String total = (String)redisTemplate.opsForValue().get(CacheKey.QRCODE_FLOAT_KEY_TOTAL + content);
//缓存数据缺失
if(receive == 0 || total == null){
if(endTime.getTime() < System.currentTimeMillis()) {
logger.info("红包已经过期:【uid:" + uid + " content:" + content + "】");
returnModel.setMessage("红包已经过期");
returnModel.setSubCode(Subcode.QECODE_OVERDUE.getSubcode());
return returnModel;
} else {
//重新从数据库读数据,恢复缓存的数据
initCache(content);
//再次请求回调方法
return putQRcodeGoldMoreMuti(returnModel, uid, content, endTime, redisTemplate);
}
}
int count = Integer.parseInt(total);
//排除初始化存入空值的影响
if ((receive - 1) > count) {
returnModel.setMessage("红包已经被领完");
returnModel.setSubCode(Subcode.QECODE_OVERDUE.getSubcode());
return returnModel;
}
//开启事务
redisTemplate.multi();
//添加领取用户记录
redisTemplate.opsForSet().add(CacheKey.QRCODE_KEY_UNIQUE + content, uid);
//获取集合长度(领取个数)
redisTemplate.opsForSet().size(CacheKey.QRCODE_KEY_UNIQUE + content);
//获取红包总个数
redisTemplate.opsForValue().get(CacheKey.QRCODE_FLOAT_KEY_TOTAL + content);
List<Object> list = redisTemplate.exec();
long add = (Long) list.get(0);
//排除初始化存入空值的影响
receive = (Long) list.get(1);
total = (String) list.get(2);
//缓存数据缺失
if (receive == 0 || total == null) {
if (endTime.getTime() < System.currentTimeMillis()) {
logger.info("红包已经过期【uid:" + uid + " content:" + content + "】");
returnModel.setMessage("红包已经过期");
returnModel.setSubCode(Subcode.QECODE_OVERDUE.getSubcode());
return returnModel;
} else {
//初始化缓存数据
initCache(content);
//再次请求
return putQRcodeGoldMoreMuti(returnModel, uid, content, endTime, redisTemplate);
}
}
if (add == 0) {
logger.info("红包已经被领过了【uid:" + uid + " content:" + content + "】");
returnModel.setMessage("红包已经被领过了");
returnModel.setSubCode(Subcode.QECODE_SCANNED.getSubcode());
return returnModel;
}
count = Integer.parseInt(total);
if ((receive - 1) > count) {
logger.info("红包已经被领完【uid:" + uid + " content:" + content + "】");
returnModel.setMessage("红包已经被领完");
returnModel.setSubCode(Subcode.QECODE_OVERDUE.getSubcode());
return returnModel;
}
}
拆红包
分配算法:0.01~(balance/count*2)红包剩余金额平均值的2倍。
当上面抢成功以后,就剩下金额分配的问题。
主要思路:使用redis事务+watch监控红包金额键,保证金额更新是一个原子操作,且避免并发引起的覆盖更新问题。
//count:剩余红包个数, redId:红包id
public void test(int count, int redId) throws Exception {
//监控红包金额key
redisTemplate.watch(“红包金额key”)。
//获取红包余额
String balance = redisTemplate.opsForValue().get("红包金额id");
//实时计算分配到的红包金额
double redMany = getRedMany(balance, count);
//开启事务
redisTemplate.multi();
//计算剩余红包金额,并更新缓存数据
balance = balance - redMany;
redisTemplate.opsForValue().setIfAbsent(“红包金额id”,balance);
List<Object> list = redisTemplate.exec();
Boolean flag= (Long) list.get(0);
if (flag == null) {
//更新失败,被人捷足先登,回调方法重新获取分配金额,直到分配成功
test(count, redId);
}
if (!flag){
//红包已经过期了
}
//todo 数据库记录的更新。
}
以上是在缓存进行拆的动作,也可以直接在数据库层面使用乐观锁机制,更新余额的时候需要比较版本号防止覆盖更新,此操作比较简单,就不再举例。最后对于拆这一个动作本人推荐使用数据库来完成,因为大部分请求会在抢的过程中过滤掉,进入到拆的请求很少,对于数据库的压力不大,相对来说会更简单点,微信红包也是直接在数据库完成拆的动作。