抢红包实战
整体架构
整体业务主要包括发红包,抢红包和点红包其整体架构如图所示:
发红包流程
发红包如微信,输入红包个数和金额,生成红包,之后就能抢红包了
抢红包流程
当在群里看到红包图案开始抢红包流程,开始判断账号合法性,如通过则从缓存中判定红包个数是否大于0,如果小于0,则表示红包被抢光了;否则表示缓存中有红包,可以抢
然后是拆红包,从缓存中随机弹出一个金额,如果金额不为空,则表示抢到红包了,缓存系统中红包个数减一,同时异步记录用户抢红包记录,如果为空,保湿红包被抢光了。
二倍均值算法
总人数为N,红包金额为M,取区间(0,M*2/N]的随机数R作为红包金额,则剩余金额为M-R,剩余人数为N-1,循环至0.
/**
* 发红包算法,金额参数以分为单位
* @param totalAmount
* @param totalPeopleNum
* @return
*/
public static List<Integer> divideRedPackage(Integer totalAmount, Integer totalPeopleNum) {
List<Integer> amountList = new ArrayList<Integer>();
if (totalAmount>0 && totalPeopleNum>0){
Integer restAmount = totalAmount;
Integer restPeopleNum = totalPeopleNum;
Random random = new Random();
for (int i = 0; i < totalPeopleNum - 1; i++) {
// 随机范围:[1,剩余人均金额的两倍),左闭右开
int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
restAmount -= amount;
restPeopleNum--;
amountList.add(amount);
}
amountList.add(restAmount);
}
return amountList;
}
Connected to the target VM, address: '127.0.0.1:50662', transport: 'socket'
总金额={1000}分,总个数={10}个
随机金额为:{47}分,即 {0.47}元
随机金额为:{5}分,即 {0.05}元
随机金额为:{193}分,即 {1.93}元
随机金额为:{174}分,即 {1.74}元
随机金额为:{44}分,即 {0.44}元
随机金额为:{67}分,即 {0.67}元
随机金额为:{219}分,即 {2.19}元
随机金额为:{34}分,即 {0.34}元
随机金额为:{132}分,即 {1.32}元
随机金额为:{85}分,即 {0.85}元
所有随机金额叠加之和={1000}分
数据库表的设计
首先是发红包记录表:
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=3 DEFAULT CHARSET=utf8 COMMENT='发红包记录';
红包明细金额表:
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=21 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 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';
发红包自测
/**
* 发红包
* @throws Exception
*/
@Override
public String handOut(RedPacketDto dto) throws Exception {
if (dto.getTotal()>0 && dto.getAmount()>0){
//生成随机金额,二倍均值
List<Integer> list=RedPacketUtil.divideRedPackage(dto.getAmount(),dto.getTotal());
//生成红包全局唯一标识,并将随机金额、个数入缓存使用list存储(使用redis)
//keyPrefix可用项目名加模块,方便区分
String timestamp=String.valueOf(System.nanoTime());
String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":").append(timestamp).toString();
redisTemplate.opsForList().leftPushAll(redId,list);
String redTotalKey = redId+":total";
redisTemplate.opsForValue().set(redTotalKey,dto.getTotal());
//异步记录红包发出的记录-包括个数与随机金额
//可以自己开一条线程实现异步,也可以使用 @Async注解实现
redService.recordRedPacket(dto,redId,list);
return redId;
}else{
throw new Exception("系统异常-分发红包-参数不合法!");
}
}
redis数据库(此处为序列化之后的数据):
mysql数据库:
红包总数:
单个金额:
抢红包自测
传入红包id和用户名可抢
/**
* 抢
*/
@RequestMapping(value = prefix+"/rob",method = RequestMethod.GET)
public BaseResponse rob(@RequestParam Integer userId, @RequestParam String redId){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
//核心业务逻辑
BigDecimal result=redPacketService.rob(userId,redId);
if (result!=null){
response.setData(result);
}else{
response=new BaseResponse(StatusCode.Fail.getCode(),"红包已被抢完!");
}
}catch (Exception e){
log.error("抢红包发生异常:userId={} redId={}",userId,redId,e.fillInStackTrace());
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
rob方法:
/**
* 不加分布式锁的情况
* 抢红包-分“点”与“抢”处理逻辑
* @param userId
* @param redId
* @return
* @throws Exception
*/
@Override
public BigDecimal rob(Integer userId,String redId) throws Exception {
ValueOperations valueOperations=redisTemplate.opsForValue();
//用户是否抢过该红包
Object obj=valueOperations.get(redId+userId+":rob");
if (obj!=null){
return new BigDecimal(obj.toString());
}
//"点红包"有红包返回true
Boolean res=click(redId);
if (res){
//"抢红包"-且红包有钱
Object value=redisTemplate.opsForList().rightPop(redId);
if (value!=null){
//红包个数减一
String redTotalKey = redId+":total";
Integer currTotal=valueOperations.get(redTotalKey)!=null? (Integer) valueOperations.get(redTotalKey) : 0;
valueOperations.set(redTotalKey,currTotal-1);
//将红包金额返回给用户的同时,将抢红包记录入数据库与缓存
BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
redService.recordRobRedPacket(userId,redId,new BigDecimal(value.toString()));
valueOperations.set(redId+userId+":rob",result,24L,TimeUnit.HOURS);
log.info("当前用户抢到红包了:userId={} key={} 金额={} ",userId,redId,result);
return result;
}
}
return null;
}
[2020-11-09 22:18:02.038] boot - INFO [http-nio-8087-exec-6] --- RedPacketService: 当前用户抢到红包了:userId=10010 key=redis:red:packet:10010:8501780044932 金额=1.95
redis:数量减少一个,同时用户的rob状态也存入redis,在抢就会提示已抢
mysql:用户抢的红包已记录
jmeter高并发压测
配置Jmeter(线程数需开多一些,如1000个):
配置http请求(由于需要多个userId来测并发,所以userId需要配置csv文件):
csv文件配置:
text文件内容:
测试(添加结果数可发现6个人既然抢完了红包,总共有十个红包):
又抢两个的,也有抢三个的,因此,在高并发情况下,存在缺陷