高并发之抢红包实战

抢红包实战

整体架构

整体业务主要包括发红包,抢红包和点红包其整体架构如图所示:

截屏2020-11-09 下午8.55.42

发红包流程

发红包如微信,输入红包个数和金额,生成红包,之后就能抢红包了

截屏2020-11-09 下午8.58.12

抢红包流程

当在群里看到红包图案开始抢红包流程,开始判断账号合法性,如通过则从缓存中判定红包个数是否大于0,如果小于0,则表示红包被抢光了;否则表示缓存中有红包,可以抢

然后是拆红包,从缓存中随机弹出一个金额,如果金额不为空,则表示抢到红包了,缓存系统中红包个数减一,同时异步记录用户抢红包记录,如果为空,保湿红包被抢光了。

截屏2020-11-09 下午9.03.55

二倍均值算法

总人数为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数据库(此处为序列化之后的数据):

截屏2020-11-09 下午9.46.57

mysql数据库:

红包总数:

截屏2020-11-09 下午9.56.11

单个金额:

截屏2020-11-09 下午9.57.52

抢红包自测

传入红包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,在抢就会提示已抢

截屏2020-11-09 下午10.19.27

mysql:用户抢的红包已记录

截屏2020-11-09 下午10.22.15

jmeter高并发压测

配置Jmeter(线程数需开多一些,如1000个):

截屏2020-11-09 下午10.45.19

配置http请求(由于需要多个userId来测并发,所以userId需要配置csv文件):

截屏2020-11-09 下午10.52.48

csv文件配置:

截屏2020-11-09 下午10.48.45

text文件内容:

截屏2020-11-09 下午10.49.24

测试(添加结果数可发现6个人既然抢完了红包,总共有十个红包):

截屏2020-11-09 下午11.11.02

又抢两个的,也有抢三个的,因此,在高并发情况下,存在缺陷

截屏2020-11-09 下午11.12.21

高并发下的优化(待续)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值