分布式中间件技术——Redis(发红包抢红包)

整体业务模块:
1,发红包模块:处理发红包的逻辑业务;
2,抢红包模块:分成点红包和拆红包模块;
3,数据库模块:发红包记录,抢红包记录,红包详情;
4,redis模块:缓存红包个数和金额;

第一步先构建数据库:创建3个表分别是
发红包记录表:主键,用户id,红包总金额,人数,全局唯一标识串,是否有效(1是是,0是否,默认是1),创建时间;
抢红包记录表:主键,红包记录id,每个红包随机金额,是否有效,创建时间;
红包详情表:主键,用户id,红包全局唯一标识串,抢红包时间,抢红包的金额,是否有效;
第二步搭建开发环境,利用mybatis的逆向工程生成表对应的实体类,操作数据的mapper和配置文件mapper.xml;

逆向工程:由于插件问题,idea一直报错,查看pom文件发现插件mybatis-generator插件出问题。找不到这个插件。换了版本依然如此,以前写的好好的逆向工程项目突然也运行不了了,可能是网络原因。使用阿里镜像依然如此。。最后使用eclipse 安装插件mybatis-generator插件 ,help–Eclipse Marketplace–搜索mybatis generator 安装插件,安装成功重启后,可以在new -others可以看到mybatis generator configuration file,这样说明插件安装成功(然后配置generatorConfig.xml文件,右键运行);

注意:WARNING: Project src does not exist Mybatis逆向工程;
与idea不同 generatorConfig.xml文件中对应的targetProject填写项目名称,idea是路径
<sqlMapGenerator targetPackage=“com.ssm.mapper” targetProject=“src\main\java”
改写为
<sqlMapGenerator targetPackage=“com.ssm.mapper” targetProject=“项目名”
表中自增ID,这个时候领域模型如果想要获取该ID的值,就需要在相应的mapper文件中添加
<insert id=“insertSelective” parameterType=“com.guo.entity.RedRecord” useGeneratedKeys=“true” keyProperty=“id”

这时候如果想继续使用idea 就可以把生成的文件copy到idea项目中即可;
使用lombok:方便编辑模板代码 @Data 使用注解,省了Getter和Setter代码的编写。。(idea首先需要安装支持lombok的插件file–settings–plugins 找到lombok安装 ,pom文件导入依赖就可以使用了;注意枚举类不能使用@Data注解 使用@Getter注解就好;

红包生成使用二倍均值算法:红包的总金额,发放的人数
public class RedPacketUtil {

public static List<Integer> divideRedPacket(Integer totalAmount,Integer totalPeopleNum){

    List<Integer> amountList = new ArrayList<>();
    if(totalAmount>0&&totalPeopleNum>0){
        Integer restAmount = totalAmount;
        Integer restPeopleNum = totalPeopleNum;
        Random random = new Random();
        //循环产生,假如10个人 循环9次,最后一个拿到剩余全部金额
        for(int i =0; i<totalPeopleNum-1;i++){
            //随机金额;二倍均值法 总金额/人数乘以2,下边这么写是左闭右开,表示随机数是[1,人均金额的2倍),红包的单个值不会超过人均金额的两倍;
            int amount = random.nextInt(restAmount/restPeopleNum*2-1)+1;
            restAmount-=amount;
            restPeopleNum--;
            //每次随机生成的金额
            amountList.add(amount);
        }
        //剩余的金额
        amountList.add(restAmount);
    }
    return amountList;
}

发红包与抢红包的业务逻辑
发红包:用户输入金额与个数,确定输入, 后台验证参数,如果正确生成红包标识串,使用红包的工具类生成随机金额的列表,产生的红包金额列表和红包个数放入到redis缓存系统中,发红包的明细表,红包记录传入数据库;

    @Override
    public String handOut(RedPacketForm form) throws Exception {
        if(form.getTotal()>0&&form.getAmount()>0){
            List<Integer> list = RedPacketUtil.divideRedPacket(form.getAmount(),form.getTotal());
            //使用时间戳作为红包标识字符串
            String timeStamp = String.valueOf(System.nanoTime());
            //生成一个随机金额列表的key
            String redId = new StringBuffer(keyPrefix).append(form.getUserId()).append(":").append(timeStamp).toString();
            //将随机金额列表放入缓存中
            redisTemplate.opsForList().leftPushAll(redId,list);
            String redTotalKey = redId+":total";
            redisTemplate.opsForValue().set(redTotalKey,form.getTotal());
            redService.recordRedPacket(form,redId,list);
            return redId;
        }else{
            throw new Exception("系统异常,参数不合法!");
        }
    }
**抢红包**:

抢红包细分两个逻辑 点红包 与 拆红包;
点红包就是查询缓存系统redis中红包的个数,有过个数大于0;说明存在红包 ,进行拆红包逻辑;否则就是红包被抢完了;
拆红包逻辑:先判断当前用户是否抢过红包,查询缓存,如果获取结果不为null说明抢过了,如果抢过了,则直接返回红包金额,没有抢过就执行点红包逻辑 ,从小红包中取出一个,然后更新缓存系统红包的个数减1,并将抢到红包的账号金额信息记录在数据库中;将当前抢到红包的用户设置到缓存系统中,表示当前用户已经抢过红包了(下次再拆就提示就不是null了,直接返回红包金额);

//判断缓冲中红包总数是否大于0个
  private Boolean click(String redId){
        String RedTotalKey = redId+":total";
        Object total = redisTemplate.opsForValue().get(RedTotalKey);
        if(total!=null&&Integer.valueOf(total.toString())>0){
            return true;
        }
        return false;
    }

抢红包
多线程问题:如果同一个用户多次发送请求,点红包。。有可能出现多次请求,查询红包个数>0;后续进行拆红包逻辑,没有抢过红包,缓存列别可能同时弹出多个小红包响应多个请求,造成一个人抢到多个红包;
解决: 使用redis.setIfAbsent()方法(对应redis的setNX)对拆红包添加分布式锁;根据redId和用户Id 生成锁的键;。同一用户多次请求拆红包的时候一次只能请求一个操作,只有第一次进行拆红包逻辑,再次请求缓存中已经存在数据,直接返回红包金额;这里加锁是为了防止一个一个用户抢多个红包问题;高并发会产生大量的key锁,对key锁设置过期时间;

    public BigDecimal rob(Integer userId, String redId) {
        Object obj = redisTemplate.opsForValue().get(redId+userId+":rob");
        if(obj!=null) {
            return new BigDecimal(obj.toString());
        }
        boolean res = click(redId);
        if(res){
            final String keyLock = redId+userId+"lock";
            boolean lock = redisTemplate.opsForValue().setIfAbsent(keyLock,redId);
            redisTemplate.expire(keyLock,24L,TimeUnit.HOURS);
            if(lock){
                Object value = redisTemplate.opsForList().rightPop(redId);
                if(value!=null){
                    String redTotalKey = redId+":total";
                    Integer currTotal = redisTemplate.opsForValue().get(redTotalKey)!=null?
                            (Integer)redisTemplate.opsForValue().get(redTotalKey) :0;
                    redisTemplate.opsForValue().set(redTotalKey,currTotal-1);

                    BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));

                    redService.recordRobRedPocket(userId,redId,new BigDecimal(value.toString()));

                    redisTemplate.opsForValue().set(redId+userId+":rob",result,24, TimeUnit.HOURS);
                    log.info("当前用户抢到红包了:userId={} key={} 金额={}",userId,redId,result);
                    return result;
                }
            }
        }
        return null;
    }

写入数据库操作可以设置为异步的:
@EnableAysnc注解在类上@Async注解在异步执行的方法上;

@Service
@EnableAsync
public class RedService implements IRedService {

    private static final Logger log = LoggerFactory.getLogger(RedService.class);
    @Autowired
    private RedRecordMapper redRecordMapper;
    @Autowired
    private RedDetailMapper redDetailMapper;
    @Autowired
    private RedRobRecordMapper redRobRecordMapper;

    @Override
    @Async
    @Transactional(rollbackFor = Exception.class)
    public void recordRedPacket(RedPacketForm form, String redId, List<Integer> list) {
        RedRecord redRecord = new RedRecord();
        redRecord.setUserId(form.getUserId());
        redRecord.setRedPaket(redId);
        redRecord.setTotal(form.getTotal());
        redRecord.setAmount(BigDecimal.valueOf(form.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);
        }

    }

@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。如果不加这个参数那么只有运行时异常才会回滚,非运行异常就是IO,SQLException,以及自定义的异常;
  Checked exception: 继承自 Exception 类是 checked exception。代码需要处理 API 抛出的 checked exception,要么用 catch 语句,要么直接用 throws 语句抛出去。不处理编译都通过;
Unchecked exception: 也称 RuntimeException,它也是继承自 Exception。但所有 RuntimeException 的子类都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作 unchecked exception;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值