基于redis 的抢红包实战

tech2022-07-06  232

抢红包

业务流程发红包流程抢红包流程业务模块划分 数据库设计红包金额随机生成算法二倍均值法 发红包模块抢红包模块高并发模块出现问题

业务流程

发红包流程

抢红包流程

业务模块划分

数据库设计

数据库脚本

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=11 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=83 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=72 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';

红包金额随机生成算法

本系统中,红包的随机金额主要是“预生成”的方式产生的,即通过给定的红包的总金额和红包个数,采用某种随机算法生成红包随机列表,并将其放在缓存中,用于拆红包的逻辑 这里随机算法采用二倍均值法

二倍均值法

这里为了保证系统每次总金额M和红包个数N生成的小红包金额是随机的且概率相等。即保证每个用户抢到的红包是随机且概率相等的。 二倍均值法的思想: 每次剩余的总金额 :M 剩余人数:N 执行 M 除 N 乘 2 得到边界值:E 然后指定一个[0,E]的随机区间,在这个随机区间产生一个随机数: R 此时将总金额M = M-R 剩余人数 N = N-1 以此类推直到N - 1 = 0; 算法流程图如下

package com.learn.boot.utils; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * 二倍均值算法生成随机红包 */ public class RedBagUtils { /** 发红包算法,金额是以分为单位的 * @param totalBagMoney 发红包总金额 * @param totalBagNumber 抢红包人数 * @return */ public static List<Integer> divideRedBag(Integer totalBagMoney,Integer totalBagNumber) { List<Integer> result = new ArrayList<>(10); if (totalBagMoney > 0 && totalBagNumber > 0) { // 记录红包的总金额,初始化时即为红包的总金额 Integer bagMoneyAmout = totalBagMoney; // 记录抢红包的总人数 Integer bagNumber = totalBagNumber; // 定义随机产生对象 Random random = new Random(); while (bagNumber -1 > 0) { // 产生随机数,[1,剩余人均金额的两倍],这里用的是最小单位分 int randMoney = random.nextInt(bagMoneyAmout/bagNumber * 2 -1 ) + 1; // 更新剩余金额 bagMoneyAmout -= randMoney; // 更新总人数 bagNumber --; result.add(randMoney); } // 循环完毕,最后一个剩余红包要加入结果集 result.add(bagMoneyAmout); } return result; } }

测试代码

package com.learn.boot.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.util.List; public class Test { private static final Logger log= LoggerFactory.getLogger(Test.class); public static void main(String[] args) { Integer money = 1000; Integer num = 10; List<Integer> result = RedBagUtils.divideRedBag(money,num); log.info("总金额 = {}分,抢红包个数={}",money,num); final Integer[] sum = {0}; result.forEach(item -> { log.info("随机金额为:{}分,即 {}元",item,new BigDecimal(item.toString()).divide(new BigDecimal(100))); sum[0] += item; }); log.info("所有随机金额加起来等于{}",sum[0]); } }

发红包模块

为了本模块的规范性和可读性,采用MVVM的模式开发相应的模块, M 模型层 V 视图层,这里暂时不用,全是接口的形式 C控制层,接收请求处理, M 中间件,采用中间件辅助

@RequestMapping("sendBag") public ResultVo handOut(@Validated @RequestBody RedPacketDto dto, BindingResult result) { // 参数校验 if (result.hasErrors()) { return ResultVo.error("参数异常"); } return iRedPacketService.handOut(dto); } @Override public ResultVo handOut(RedPacketDto dto) { try { //采用二倍均值法生成随机金额列表(在上一节已经采用代码实现了二倍均值 法) List<Integer> list = RedBagUtils.divideRedBag(dto.getAmount(), dto.getTotal()); //生成红包全局唯一标识串 String timestamp=String.valueOf(System.nanoTime()); //根据缓存Key的前缀与其他信息拼接成一个新的用于存储随机金额列表的Key String redId = new StringBuffer("red:packet:").append(dto. getUserId()).append(":").append(timestamp).toString(); // 将随机金额列表存入缓存列表中 redisTemplate.opsForList().leftPushAll(redId,list); // 设置一天的时间戳 redisTemplate.expire(redId, 65535, TimeUnit.SECONDS); // 根据缓存Key的前缀与其他信息拼接成一个新的用于存储红包总数的Key String redTotalKey = redId+":total"; // 将红包总数存入缓存中 redisTemplate.opsForValue().set(redTotalKey,dto.getTotal()); // 设置一天的时间戳 redisTemplate.expire(redTotalKey, 65535, TimeUnit.SECONDS); // 异步记录红包的全局唯一标识串、红包个数与随机金额列表入数据库 redService.recordRedPacket(dto,redId,list); //将红包的全局唯一标识串返回给前端 return ResultVo.success("发送红包成功",redId); }catch (Exception ex) { log.error("发红包异常"); return ResultVo.error("发红包异常"); } } @Async @Override @Transactional(rollbackFor = Exception.class) public void recordRedPacket(RedPacketDto dto, String redId, List<Integer> list) throws Exception { //定义实体类对象 RedRecord redRecord=new RedRecord(); //设置字段的取值信息 redRecord.setUserId(dto.getUserId()); redRecord.setRedPacket(redId); redRecord.setTotal(dto.getTotal()); redRecord.setAmount(BigDecimal.valueOf(dto.getAmount())); redRecord.setCreateTime(new Date()); //将对象信息插入数据库 redRecordMapper.insertSelective(redRecord); //定义红包随机金额明细实体类对象 RedDetail detail; //遍历随机金额列表,将金额等信息设置到相应的字段中 for (Integer i:list){ detail=new RedDetail(); detail.setRecordId(redRecord.getId()); detail.setAmount(BigDecimal.valueOf(i)); detail.setCreateTime(new Date()); //将对象信息插入数据库 redDetailMapper.insertSelective(detail); } }

抢红包模块

@RequestMapping("robRedBag") public ResultVo robRedBag(@RequestBody RobRedPacketDto dto) { return iRedPacketService.robRedBag(dto); } package com.learn.boot.dto; /** * 抢红包请求的参数 */ import com.sun.istack.internal.NotNull; import lombok.Data; import javax.validation.constraints.Min; @Data public class RobRedPacketDto { @NotNull // 用户id private Integer userId; @NotNull // 红包Id private String redId; } /** 抢红包逻辑 * @return */ @Override public ResultVo robRedBag(RobRedPacketDto dto) { try { //定义Redis操作组件的值操作方法 ValueOperations valueOperations = redisTemplate.opsForValue(); //在处理用户抢红包之前,需要先判断一下当前用户是否已经抢过该红包了 //如果已经抢过了,则直接返回红包金额,并在前端显示出来 BigDecimal obj = (BigDecimal) valueOperations.get(dto.getRedId() + dto.getUserId() + ":rob"); if (obj != null) { // log.info("userId = {} 成功抢到,金额为{}",dto.getUserId(),obj.toString()); return ResultVo.error("您已成功抢到",new BigDecimal(obj.toString())); } // 判断是否可以抢 Boolean res = click(dto.getRedId()); // true表示可以抢 if (!res) { return ResultVo.error("红包被抢完啦"); } // 加入setnx命令,实现分布式锁 final String lockKey = dto.getRedId() + dto.getUserId() +"-lock"; //调用setIfAbsent()方法,间接实现分布式锁 Boolean lock=valueOperations.setIfAbsent(lockKey,dto.getRedId()); // 得到锁了就执行下面的操作 if (!lock) { log.error("手速太快"); return ResultVo.error(ResultCode.FORBIDDEN); } // 先从随机金币列表中随便弹出一个值 Integer money = (Integer)redisTemplate.opsForList().leftPop(dto.getRedId()); if (money == null) { return ResultVo.error("手慢了"); } // 如果有值,更新红包剩余个数 String redTotalKey = dto.getRedId()+":total"; Integer restNumberBag = (Integer) valueOperations.get(redTotalKey); valueOperations.set(redTotalKey,restNumberBag - 1); BigDecimal result = new BigDecimal(money). divide(new BigDecimal(100)); //记录抢到红包时用户的账号信息以及抢到的金额等信息入数据库 redService.recordRobRedPacket(dto.getUserId(),dto.getRedId(),new BigDecimal(restNumberBag)); //将当前抢到红包的用户信息放置进缓存系统中,用于表示当前用户已经抢过红 包了 valueOperations.set(dto.getRedId() + dto.getUserId() +":rob",result,24L,TimeUnit. HOURS); //输出当前用户抢到红包的记录信息 log.info("当前用户抢到红包了:userId={} key={} 金额={} ",dto.getUserId(), dto.getUserId(),result); //将结果返回 return ResultVo.success("抢红包成功",result); }catch (Exception ex) { log.error("抢红包异常",ex); return ResultVo.error("抢红包异常"); } } } /** 判断是否可以抢 * @param redId * @return */ private Boolean click(String redId) { //定义Redis的Bean操作组件(值操作组件) ValueOperations valueOperations=redisTemplate.opsForValue(); //定义用于查询缓存系统中红包剩余个数的Key //这在上一节“发红包”业务模块中已经指定过了 String redTotalKey = redId+":total"; //获取缓存系统Redis中红包剩余个数 Object total=valueOperations.get(redTotalKey); //判断红包剩余个数total是否大于0,如果大于0,则返回true,表示还有红包 if (total != null && Integer.valueOf(total.toString()) > 0){ return true; } //返回false,则表示已经没有红包可抢了 return false; } /** 抢红包记录入库 * @param userId * @param redId * @param amount */ @Override @Async public void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) { //定义记录抢到红包时录入相关信息的实体对象,并设置相应字段的取值 RedRobRecord redRobRecord=new RedRobRecord(); redRobRecord.setUserId(userId); redRobRecord.setRedPacket(redId); redRobRecord.setAmount(amount); redRobRecord.setRobTime(new Date()); //将实体对象信息插入数据库中 redRobRecordMapper.insertSelective(redRobRecord); }

高并发模块出现问题

出现了一个用户抢了多个红包

原因在于

// 先从随机金币列表中随便弹出一个值 Integer money = (Integer)redisTemplate.opsForList().leftPop(dto.getRedId()); if (money == null) { return ResultVo.error("手慢了"); }

这段代码存在高并发带来的问题,假设有两个用户id的一样进来了,那么就从随机金币列表取到了两个值,并且入库 解决方法 redis 的setNx ,完好的解决了这个问题,在不存在的时候入缓存,如果之前存在过就返回false

最新回复(0)