本篇博客打算在Redis中间件的基础上,引入一个发抢红包的流程,借助一个简单的实例,熟悉Redis的一些使用。
主要分为发红包和抢红包流程,针对红包的操作记录,我们需要定义三张数据表,分别如下
红包详情表
-- 红包的详情记录,随机分成的每个小金额的红包都会在这个表格中记录
CREATE TABLE `red_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`record_id` int(11) NOT NULL COMMENT '红包记录id',
`amount` decimal(8,2) DEFAULT NULL COMMENT '每个小红包的金额(单位为分)',
`is_active` tinyint(4) DEFAULT '1',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=193 DEFAULT CHARSET=utf8 COMMENT='红包明细金额';
发送红包记录表
-- 发送红包的记录表,每个发送红包的个数和金额都会在这个表中记录
CREATE TABLE `red_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户id',
`red_packet` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '红包全局唯一标识串',
`total` int(11) NOT NULL COMMENT '人数',
`amount` decimal(10,2) DEFAULT NULL COMMENT '总金额(单位为分)',
`is_active` tinyint(4) DEFAULT '1',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 COMMENT='发红包记录';
抢红包记录表
-- 抢红包的记录表,什么客户抢了那一个红包,都会在这张表中记录
CREATE TABLE `red_rob_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT '用户账号',
`red_packet` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '红包标识串',
`amount` decimal(8,2) DEFAULT NULL COMMENT '红包金额(单位为分)',
`rob_time` datetime DEFAULT NULL COMMENT '时间',
`is_active` tinyint(4) DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=178 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';
用户在输入红包个数和金额之后,后台自动生成红包唯一标识串,然后自动分派好每个红包的金额,异步记录每个红包金额记录。
整体流程如下所示:
其实流程并不复杂,客户在页面上输入红包个数和金额,后续的矩形部分才是我们需要重点关注的,需要按照指定个数生成随机金额的红包。然后异步记录红包的发送记录到数据库和缓存。
代码如下:
@Transactional(rollbackFor = Exception.class)
public String handOut(RedPacketDto redPacketDto) {
if (redPacketDto.getCount() > 0 && redPacketDto.getAmount() > 0) {
//生成全局的红包id
String redKey = String.format(RedPacketKey, redPacketDto.getUserId(), SNOWFLAKE.nextIdStr());
//生成红包的随机金额
List<Integer> redAmountList = RedPacketUtil.divideRedPackage(redPacketDto.getCount(), redPacketDto.getAmount());
recordRedPacket(redPacketDto, redAmountList, redKey);
//红包金额存入缓存
saveRedAmount2Cache(redPacketDto, redAmountList, redKey);
return redKey;
}
return null;
}
/**
保存发红包的记录,和记录生成的每个随机金额的红包
*/
@Async
public void recordRedPacket(RedPacketDto redPacketDto, List<Integer> redAmountList, String redKey) {
RedRecord entity = new RedRecord();
entity.setCreateTime(DateTime.now().toSqlDate());
entity.setUserId(redPacketDto.getUserId());
entity.setRedPacket(redKey);
entity.setTotal(redPacketDto.getCount());
entity.setAmount(BigDecimal.valueOf(redPacketDto.getAmount()));
redRecordMapper.insertSelective(entity);//保存发红包人所发的金额记录.
//保存每一个红包记录
redAmountList.parallelStream().forEach(redAmount -> {
RedDetail detail = new RedDetail();
detail.setRecordId(entity.getId());
detail.setAmount(BigDecimal.valueOf(redAmount));
redDetailMapper.insertSelective(detail);
});
}
/**
将生成的红包个数记录到缓存
*/
@Async
public void saveRedAmount2Cache(RedPacketDto redPacketDto, List<Integer> redAmountList, String redPacketKey) {
//存入中的红包金额
redisTemplate.opsForValue().set(redPacketKey + ":total", redPacketDto.getCount());
//用一个list存放所有的小红包金额
redisTemplate.opsForList().leftPushAll(redPacketKey, redAmountList);
}
红包金额的生成采用二倍均值法,每次用红包所剩金额处于待分配红包个数的两倍,即可,算法比较简单,并不复杂,具体流程如下所示:
将这个算法流程变成代码,也并不复杂
@Slf4j
public class RedPacketUtil {
/**
* 拆分红包的方法
*
* @param totalCount 拆分成的红包个数
* @param totalMoney 总的红包金额
* @return
*/
public static List<Integer> divideRedPackage(int totalCount, int totalMoney) {
List<Integer> amountList = Lists.newLinkedList();
if (totalCount > 0 && totalMoney > 0) {
int restMoney = totalMoney;
int restCount = totalCount;
int redAmount = 0;
Random random = new Random();
while (restCount > 1) {
//由于随机数本身的机制,无法在固定的循环次数之内分配完成所有金额,因此这里循环n-1次
redAmount = random.nextInt(restMoney / restCount * 2 - 1) + 1;
amountList.add(redAmount);
restMoney -= redAmount;
restCount--;
}
//这里将剩余金额作为最后一个红包。
amountList.add(restMoney);
}
log.info("最终分配的结果为:{}",amountList);
return amountList;
}
}
这里只是顺便提一句,为了保证生成的红包id唯一,这里用到了雪花算法,相关算法代码百度即可找到,这里为了篇幅不再贴出。
基本的抢红包流程其实也不复杂,先判断用户是否抢过红包以及红包是否存在的校验,如果不存在则直接返回。之后从缓存中随机获取一个红包金额,如果金额大于0,表示抢到了红包,这个时候减少红包个数,并且异步记录抢红包的记录。
直接上代码
/**
* 抢红包
* @param userId
* @param redPacketKey
* @return
*/
public Integer robRedPacket(final Integer userId,final String redPacketKey){
ValueOperations valueOperation = redisTemplate.opsForValue();
//判断当前用户是否抢过红包
final String redUserRobbedKey = redPacketKey+userId+":rob";
Object isRobbedAmount = valueOperation.get(redUserRobbedKey);
if(isRobbedAmount!=null){
log.info("用户:{},已经抢过该红包了",userId);
return 0;
}
boolean existRedPacket = isExistRedPacket(redPacketKey);
if(existRedPacket){
Object getRedValue = redisTemplate.opsForList().rightPop(redPacketKey);
if(getRedValue!=null){
//红包个数减一
final String redCountKey = redPacketKey+":total";
valueOperation.increment(redCountKey,-1L);
//抢红包记录入库
final Integer getRedAmount = Integer.valueOf(String.valueOf(getRedValue));
recordRobRedPacket(userId,redPacketKey,getRedAmount);
//记录当前用户获取的红包记录,避免同一个用户重复获取红包
valueOperation.set(redUserRobbedKey,getRedValue,24L,TimeUnit.HOURS);
log.info("用户:{},抢到了红包,红包id为:{},抢到的金额为:{}",userId,redPacketKey,getRedAmount);
return getRedAmount;
}
}else{
log.info("用户:{},来晚一步,红包被抢光了",userId);
}
return 0;
}
/**
* 判断缓存中红包的个数是否大于0
* @param redPacketKey
* @return
*/
private boolean isExistRedPacket(final String redPacketKey) {
Object total = redisTemplate.opsForValue().get(redPacketKey + ":total");
if (total != null && Integer.valueOf(String.valueOf(total)) > 0) {
return true;
}
return false;
}
/**
* 保存抢红包的金额记录,异步记录
* @param userId
* @param redPacketKey
* @param amount
*/
@Async
public void recordRobRedPacket(final Integer userId,final String redPacketKey,final Integer amount){
RedRobRecord entity = new RedRobRecord();
entity.setUserId(userId);
entity.setRedPacket(redPacketKey);
entity.setAmount(BigDecimal.valueOf(amount));
entity.setRobTime(DateTime.now());
redRobRecordMapper.insertSelective(entity);
}
上述代码其实还有些问题,通过JMeter进行压力测试,在1秒内并发2000个请求(微信群最多500人,这里2000的并发请求,算是比较极限了),会出现同一个客户抢同一个红包多次的情况,如下所示(下述结果直接取自数据库)。
为了解决这个问题,还是需要引入分布式锁,关于Redis实现分布式锁其实比较简单,之前总结过 ——基于Redis实现分布式锁实例。
/**
* 一个小的随机红包 每个人只能抢一次;一个人每次只能抢到一个小的随机红包金额 - 永远保证1:1的关系
* @param userId
* @param redPacketKey
* @return
*/
public Integer robRedPackageVersion03(final Integer userId,final String redPacketKey){
ValueOperations valueOperations = redisTemplate.opsForValue();
final String redUserRobbedKey = redPacketKey+userId+":rob";
Object isRobbedAmount = valueOperations.get(redUserRobbedKey);
if(isRobbedAmount!=null){
log.info("用户:{},已经抢过该红包了",userId);
return 0;
}
boolean existRed = isExistRedPacket(redPacketKey);
if(existRed){
final String lockKey = redPacketKey+userId+"-lock";
boolean lock =valueOperations.setIfAbsent(lockKey,redPacketKey);//底层调用的是setNx
try{
//TODO:抢红包的部门加入分布式锁
if(lock){
redisTemplate.expire(lockKey,48L,TimeUnit.HOURS);
Object getRedValue = redisTemplate.opsForList().rightPop(redPacketKey);
if(getRedValue!=null){
//红包个数减一
final String redCountKey = redPacketKey+":total";
valueOperations.increment(redCountKey,-1L);
//抢红包记录入库
final Integer getRedAmount = Integer.valueOf(String.valueOf(getRedValue));
recordRobRedPacket(userId,redPacketKey,getRedAmount);
//记录当前用户获取的红包记录,避免同一个用户重复获取红包
valueOperations.set(redUserRobbedKey,getRedValue,24L,TimeUnit.HOURS);
log.info("用户:{},抢到了红包,红包id为:{},抢到的金额为:{}",userId,redPacketKey,getRedAmount);
return getRedAmount;
}
}
}catch (Exception e){
}finally{
//TODO:这里不需要释放锁,因为红包一发出去,就是一个新的key;一旦被抢完,生命周期永远终止
}
}
return 0;
}
到这一步,才完成真正的抢红包操作。注意,这里并不需要释放锁(毕竟抢红包针对一个用户也只是一次性的操作),只需要给分布式锁设置一个过期时间,到时候会自动释放。
其实分布式锁其实还有其他的实现方式Redission和zookeeper的实现方式。Redission实现起来更为简介,这个后面会对这个中间级进行总结。
上述实例代码中,都是基于红包被抢光的前提。但是真正的实际操作中,客户发了红包之后,24小时之后会回退,这个功能可以通过定时任务和消息中间件的延迟队列来实现,这里篇幅所限不再总结。至此,关于Redis中大部分的总结算是告一段落,还有一个AOF的持久化机制,这个其实我觉得如果不是专业的运维人员简单了解一下就行。本系列涉及的springboot中操作Redis的源码提供免积分下载,下载地址为
springboot-reids 代码实例下载地址