基于redis 的抢红包实战

业务流程

在这里插入图片描述

发红包流程

在这里插入图片描述

抢红包流程

在这里插入图片描述

业务模块划分

在这里插入图片描述

数据库设计

在这里插入图片描述
数据库脚本

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值