学习基于springboot的java分布式中间件-Redis(4) redis之抢红包案例实战

抢红包系统业务流程

一个抢红包系统主要由以下三大部分组成

  • 信息流 : 包括用户操作背后的通信请求和红包信息在不同用户与群中的流转等
  • 业务流 : 包括发红包、点红包和抢红包等业务逻辑
  • 资金流 : 包括红包背后的资金转账和入账等流程

场景模拟:用户发出一个固定金额的红包,让若干个人抢。

业务流程分析

主要有两大业务模块组成:发红包和抢红包,其中抢红包又可以拆分成两个小业务,即:用户点红包和用户拆红包。

首先是用户发红包,用户进入某个群,然后点击红包按钮输入总金额与红包个数,点击塞红包按钮,输入支付密码后将在群里生成红包,然后群友就可以开始抢红包了。

然后是用户抢红包流程,当群里的成员看见红包的时候,正常情况下都会点击红包,由此开始系统的抢红包流程。系统后端接口在接收到前端用户抢红包的请求,首先要校验用户账号的合法性(当前账号是否已被禁止抢红包等),是否绑定银行卡信息的校验,当全部信息通过校验之后,开启真正的抢红包业务逻辑处理。

(1)首先是用户点红包业务,主要是判断缓存系统中红包个数是否大于0.如果小于等于0,意味着红包已经抢光了,如果大于0,则表示缓存系统中还有红包,还可以抢。

(2)最后是用户用户拆红包业务的逻辑,主要是从缓存系统中的红包随机金额队列中弹出一个随机金额,如果金额不为空,则表示用户抢到红包了,缓存系统中红包个数减一。同时异步记录用户抢红包的记录并结束流程,如果金额为空,则意味着用户来晚一步,红包已经抢光了(比如用户点开红包后,却迟迟没有拆开的情况)

业务模块划分

整体业务模块包括:

  • 发红包模块:主要包括接收并处理用户发红包请求逻辑的处理
  • 抢红包模块:主要包括用户点红包和拆红包请求逻辑的处理
  • 数据操作DB模块:主要包括系统整体业务逻辑处理过程中数据的记录
  • 缓存中间件Redis模块:主要用于缓存红包个数以及红包随机金额

在系统整体业务操作过程中,将产生各种的核心业务数据,这部分数据将会由数据操作DB模块存储至数据库中,最后是缓存中间件Redis模块,主要用于发红包时候缓存红包个数和由随机算法产生的红包随机金额列表,同时借助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=16 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=133 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=118 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';

PS: 为了系统整体的实战方便,这里的金额,包括红包总金额、每个红包随机金额和抢到红包时候的金额,在数据表的存储采用“分”作为存储单位,一元等于100分,这一点在后面介绍二倍均值法生成随机金额时候将会有所介绍。

环境搭建

三个实体类:

package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedRecord {
    private Integer id; //主键id

    private Integer userId; //用户id

    private String redPacket; //红包全局唯一标识串

    private Integer total; //红包指定可以抢的总人数

    private BigDecimal amount; //红包总金额

    private Byte isActive; //是否有效

    private Date createTime; //创建时间


}
package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedDetail {
    private Integer id; //主键id

    private Integer recordId; //红包记录id

    private BigDecimal amount; //红包随机金额

    private Byte isActive; //是否有效

    private Date createTime; //创建时间


}
package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedRobRecord {
    private Integer id; //主键id

    private Integer userId; //用户id

    private String redPacket; //红包全局唯一标识串

    private BigDecimal amount; //抢到的红包金额

    private Date robTime; //抢到时间

    private Byte isActive; //是否有效

}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedDetail;

public interface RedDetailMapper {

    int deleteByPrimaryKey(Integer id);

    int insert(RedDetail record);

    int insertSelective(RedDetail record);

    RedDetail selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedDetail record);

    int updateByPrimaryKey(RedDetail record);
}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedRecord;

public interface RedRecordMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(RedRecord record);

    int insertSelective(RedRecord record);

    RedRecord selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedRecord record);

    int updateByPrimaryKey(RedRecord record);
}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedRobRecord;

public interface RedRobRecordMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(RedRobRecord record);

    int insertSelective(RedRobRecord record);

    RedRobRecord selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedRobRecord record);

    int updateByPrimaryKey(RedRobRecord record);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedDetailMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedDetail" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="record_id" property="recordId" jdbcType="INTEGER" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, record_id, amount, is_active, create_time
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from red_detail
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_detail
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedDetail" >
    insert into red_detail (id, record_id, amount, 
      is_active, create_time)
    values (#{id,jdbcType=INTEGER}, #{recordId,jdbcType=INTEGER}, #{amount,jdbcType=DECIMAL}, 
      #{isActive,jdbcType=TINYINT}, #{createTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" parameterType="com.zwx.middleware.entity.RedDetail" >
    insert into red_detail
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="recordId != null" >
        record_id,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
      <if test="createTime != null" >
        create_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="recordId != null" >
        #{recordId,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        #{createTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedDetail" >
    update red_detail
    <set >
      <if test="recordId != null" >
        record_id = #{recordId,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedDetail" >
    update red_detail
    set record_id = #{recordId,jdbcType=INTEGER},
      amount = #{amount,jdbcType=DECIMAL},
      is_active = #{isActive,jdbcType=TINYINT},
      create_time = #{createTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedRecordMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedRecord" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="user_id" property="userId" jdbcType="INTEGER" />
    <result column="red_packet" property="redPacket" jdbcType="VARCHAR" />
    <result column="total" property="total" jdbcType="INTEGER" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user_id, red_packet, total, amount, is_active, create_time
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select
    <include refid="Base_Column_List" />
    from red_record
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_record
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedRecord" >
    insert into red_record (id, user_id, red_packet,
      total, amount, is_active,
      create_time)
    values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{redPacket,jdbcType=VARCHAR},
      #{total,jdbcType=INTEGER}, #{amount,jdbcType=DECIMAL}, #{isActive,jdbcType=TINYINT},
      #{createTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" parameterType="com.zwx.middleware.entity.RedRecord" >
    insert into red_record
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userId != null" >
        user_id,
      </if>
      <if test="redPacket != null" >
        red_packet,
      </if>
      <if test="total != null" >
        total,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
      <if test="createTime != null" >
        create_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userId != null" >
        #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="total != null" >
        #{total,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        #{createTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedRecord" >
    update red_record
    <set >
      <if test="userId != null" >
        user_id = #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        red_packet = #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="total != null" >
        total = #{total,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedRecord" >
    update red_record
    set user_id = #{userId,jdbcType=INTEGER},
      red_packet = #{redPacket,jdbcType=VARCHAR},
      total = #{total,jdbcType=INTEGER},
      amount = #{amount,jdbcType=DECIMAL},
      is_active = #{isActive,jdbcType=TINYINT},
      create_time = #{createTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedRobRecordMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedRobRecord" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="user_id" property="userId" jdbcType="INTEGER" />
    <result column="red_packet" property="redPacket" jdbcType="VARCHAR" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="rob_time" property="robTime" jdbcType="TIMESTAMP" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user_id, red_packet, amount, rob_time, is_active
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from red_rob_record
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_rob_record
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    insert into red_rob_record (id, user_id, red_packet, 
      amount, rob_time, is_active
      )
    values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{redPacket,jdbcType=VARCHAR}, 
      #{amount,jdbcType=DECIMAL}, #{robTime,jdbcType=TIMESTAMP}, #{isActive,jdbcType=TINYINT}
      )
  </insert>
  <insert id="insertSelective" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    insert into red_rob_record
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userId != null" >
        user_id,
      </if>
      <if test="redPacket != null" >
        red_packet,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="robTime != null" >
        rob_time,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userId != null" >
        #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="robTime != null" >
        #{robTime,jdbcType=TIMESTAMP},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    update red_rob_record
    <set >
      <if test="userId != null" >
        user_id = #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        red_packet = #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="robTime != null" >
        rob_time = #{robTime,jdbcType=TIMESTAMP},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    update red_rob_record
    set user_id = #{userId,jdbcType=INTEGER},
      red_packet = #{redPacket,jdbcType=VARCHAR},
      amount = #{amount,jdbcType=DECIMAL},
      rob_time = #{robTime,jdbcType=TIMESTAMP},
      is_active = #{isActive,jdbcType=TINYINT}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>

在这里插入图片描述
api模块中添加响应类

package com.zwx.middleware.enums;

/**
 * 通用状态码类
 */
public enum StatusCode {
    //以下是暂时设定的几种状态码类
    Success(0,"成功"),
    Fail(-1,"失败"),
    InvalidParams(201,"非法的参数!"),
    InvalidGrantType(202,"非法的授权类型");

    //状态码
    private Integer code;
    //描述信息
    private String msg;
    //重载的构造方法
    StatusCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

package com.zwx.middleware.response;


import com.zwx.middleware.enums.StatusCode;


public class BaseResponse<T> {
    //状态码
    private Integer code;
    //描述信息
    private String msg;
    //响应数据-采用泛型表示可以接受通用的数据类型
    private T data;
    //重载的构造方法一
    public BaseResponse(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    //重载的构造方法二
    public BaseResponse(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }
    //重载的构造方法三
    public BaseResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

红包金额 随机生成算法

二倍均值法生成红包随机金额的算法。

公式:(0, M/N * 2),M为剩余红包金额,N为剩余人数,这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。

在这里插入图片描述

package com.zwx.middleware.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 二倍均值法的代码实战
 */
public class RedPacketUtil {

    /**
     * 发红包算法,金额参数以分为单位
     *
     * @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();
            //不断循环遍历、迭代更新地产生随机金额,知道N-1<=0
            for (int i = 0; i < totalPeopleNum - 1; i++) {
                // 随机范围:[1,剩余人均金额的两倍),左闭右开
                //随机金额R、单位为分
                int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
                //更新剩余的总金额M=M-R
                restAmount -= amount;
                //更新剩余的总人数N=N-1
                restPeopleNum--;
                //将产生的随机金额添加进列表中
                amountList.add(amount);
            }
            //循环完毕,剩余的金额为最后一个随机金额,也需要将其添加进列表
            amountList.add(restAmount);
        }

        //最终产生的随机金额返回
        return amountList;
    }

}

PS:如果发红包输入金额为10元,那么调用总金额参数需要赋值为1000分

算法自测

package com.zwx.middleware;

import com.zwx.middleware.utils.RedPacketUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.math.BigDecimal;
import java.util.List;

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
@Slf4j
public class RedPacketTest {

    //二倍均值法自测
    @Test
    public void one() throws Exception{
        //总金额单位为分
        Integer amout=1000;
        //总人数-红包个数
        Integer total=10;
        //得到随机金额列表
        List<Integer> list=RedPacketUtil.divideRedPackage(amout,total);
        log.info("总金额={}分,总个数={}个",amout,total);

        //用于统计生成的随机金额之和是否等于总金额
        Integer sum=0;
        //遍历输出每个随机金额
        for (Integer i:list){
            log.info("随机金额为:{}分,即 {}元",i,new BigDecimal(i.toString()).divide(new BigDecimal(100)));
            sum += i;
        }
        log.info("所有随机金额叠加之和={}分",sum);
    }
}

在这里插入图片描述

发红包模块

核心的处理逻辑为在接收前端发红包着设定的红包总金额M和总个数M,后端根据这2个参数,采用二倍均值法生成N个随机金额的红包,最后将红包个数N与随机金额列表存到缓存中,同时将相关数据异步记录到数据库中。

此外,后端接口在收到前端用户发红包的请求时候,将采用当前的时间戳(纳秒级)作为红包全局唯一标识串,并返回给前端,后续用户发起抢红包的请求时候,将会带上这一参数,目的是为了给发出的红包作为标记,并根据这一标记去缓存中查询红包个数和随机金额列表等数据。

在这里插入图片描述
处理发红包的请求时,后端接口需要接收红包总金额和总个数等参数,故而将其封装成实体对象RedPacketDto

package com.zwx.middleware.dto;

import lombok.Data;
import lombok.ToString;

import javax.validation.constraints.NotNull;


@Data
@ToString
public class RedPacketDto {

    private Integer userId;

    //指定多少人抢
    @NotNull
    private Integer total;

    //指定总金额-单位为分
    @NotNull
    private Integer amount;
}

@RestController
@Slf4j
public class RedPacketController {


    //定义请求前缀
    private static final String prefix="red/packet";

    @Autowired
    private IRedPacketService redPacketService;


    /**
     * 发
     */
    @PostMapping(value = prefix+"/hand/out")
    public BaseResponse handOut(@Validated @RequestBody RedPacketDto dto, BindingResult result){
        //参数校验
       if (result.hasErrors()){
           return new BaseResponse(StatusCode.InvalidParams);
       }
       BaseResponse response=new BaseResponse(StatusCode.Success);
       try {
           //核心业务逻辑处理服务,最终返回红包全局唯一标识符
            String redId=redPacketService.handOut(dto);
            //将红包全局唯一标识符返回前端
            response.setData(redId);

       }catch (Exception e){
           //如果报异常则输出日志并且返回相应的错误信息
           log.error("发红包发生异常:dto={} ",dto,e.fillInStackTrace());
           response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
       }
       return response;
    }


package com.zwx.middleware.service;


import com.zwx.middleware.dto.RedPacketDto;

import java.math.BigDecimal;


public interface IRedPacketService {

    //发红包核心业务逻辑的实现
    String handOut(RedPacketDto dto) throws Exception;
    //抢红包(在下一节中将实现该方法)
    BigDecimal rob(Integer userId, String redId) throws Exception;
}

@Service
@Slf4j
public class RedPacketService implements IRedPacketService {


    private final SnowFlake snowFlake=new SnowFlake(2,3);

    //存储至缓存系统Redis时定义的key
    private static final String keyPrefix="redis:red:packet:";


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IRedService redService;


    /**
     * 发红包
     * @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());

            //生成红包全局唯一标识,并将随机金额、个数入缓存
            String timestamp=String.valueOf(System.nanoTime());
            //根据缓存key的前缀与其他信息拼成一个新的用于存储随机金额列表的key
            String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":").append(timestamp).toString();
            //将随机金额列表存入缓存列表中
            redisTemplate.opsForList().leftPushAll(redId,list);
            //根据缓存key的前缀与其他信息拼成一个新的用于存储红包总数的key
            String redTotalKey = redId+":total";
            //将红包总数存入缓存中
            redisTemplate.opsForValue().set(redTotalKey,dto.getTotal());

            //异步记录红包发出的记录-包括个数与随机金额
            redService.recordRedPacket(dto,redId,list);

            return redId;
        }else{
            throw new Exception("系统异常-分发红包-参数不合法!");
        }
    }

package com.zwx.middleware.service;


import com.zwx.middleware.dto.RedPacketDto;

import java.math.BigDecimal;
import java.util.List;

/**
 * 红包记录服务
 */
public interface IRedService {

    //记录发红包时红包的全局唯一标识串、随机金额列表和红包个数等信息入数据库
    void recordRedPacket(RedPacketDto dto, String redId, List<Integer> list) throws Exception;
    //记录抢红包时用户抢到的红包金额等信息入数据库
    void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) throws Exception;

}


@Service
@EnableAsync
public class RedService implements IRedService {

    @Resource
    private RedRecordMapper redRecordMapper;

    @Resource
    private RedDetailMapper redDetailMapper;

    @Resource
    private RedRobRecordMapper redRobRecordMapper;


    /**
     * 发红包记录
     * @param dto
     * @param redId
     * @param list
     * @throws Exception
     */
    @Override
    @Async
    @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()));
        redRecordMapper.insertSelective(redRecord);

        RedDetail detail;
        for (Integer i:list){
            detail=new RedDetail();
            detail.setRecordId(redRecord.getId());
            detail.setAmount(BigDecimal.valueOf(i));
            redDetailMapper.insertSelective(detail);
        }
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

抢红包模块实战

业务分析

(1)从业务的角度分析,抢红包业务模块需要实现两大业务逻辑,包括点红包业务逻辑和拆红包业务逻辑,通俗地讲,他包含两大动作。

(2)从系统架构的角度分析,抢红包业务模块对应的后端处理逻辑需要保证接口的稳定性、可拓展新和抗高并发性,对于相应接口的设计需要尽可能做到低耦合和服务的高内聚,故而在代码实战过程中,采用的是面向对象接口,服务进行编程。

(3)从技术的角度分析,抢红包业务模块对应的后端接口需要频繁地访问缓存系统Redis,用于获取红包剩余个数和剩余金额列表,进而用于判断用户点击红包,拆红包是否成功,除此之外,在每次用户成功抢到红包之后,后端接口需要及时更新缓存系统中的红包的剩余个数,记录相应的信息入数据库等。
在这里插入图片描述

 /**
     * 抢
     */
    @GetMapping( prefix+"/rob")
    public BaseResponse rob(@RequestParam Integer userId, @RequestParam String redId){
        BaseResponse response=new BaseResponse(StatusCode.Success);
        try {
            //调用红包业务逻辑处理接口中的抢红包方法,最终返回抢到的红包的金额
            //单位为元(不为Null时则表示抢到了,否则代表已经被抢完了)
            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;
    }
/**
     * 不加分布式锁的情况
     * 抢红包-分“点”与“抢”处理逻辑
     * @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());
        }

        //"点红包"
        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;
    }


    /**
     * 点红包-返回true,则代表红包还有,个数>0
     * @throws Exception
     */
    private Boolean click(String redId) throws Exception{
        ValueOperations valueOperations=redisTemplate.opsForValue();

        String redTotalKey = redId+":total";
        Object total=valueOperations.get(redTotalKey);
        if (total!=null && Integer.valueOf(total.toString())>0){
            return true;
        }
        return false;
    }
 /**
     * 抢红包记录
     * @param userId
     * @param redId
     * @param amount
     * @throws Exception
     */
    @Override
    @Async
    public void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) throws Exception {
        RedRobRecord redRobRecord=new RedRobRecord();
        redRobRecord.setUserId(userId);
        redRobRecord.setRedPacket(redId);
        redRobRecord.setAmount(amount);
        redRobRecord.setRobTime(new Date());
        redRobRecordMapper.insertSelective(redRobRecord);
    }

测试:
http://localhost:8087/middleware/red/packet/rob?userid=10010&redid=redis:red:packet:10010:31156749047566
在这里插入图片描述
在这里插入图片描述
换一个userId
在这里插入图片描述
在这里插入图片描述
跟换userId直到10次满
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

JMeter压测高并发抢红包

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
事实证明在高并发的情况下,比如说魔偶一时刻同一用户在界面疯狂的点击红包图样时,如果前端不加以控制,同一时刻同一用户将发起多个请求,后端接收后很可能同时进行缓存系统中是否有红包的判断并成功通过,然后执行后面弹出红包随机金额的业务逻辑,导致一个用户抢到多个红包。

优化方案

基于Redis的分布式锁解决高并发的安全问题
传统单体java应用中,未了解决多线程高并发的安全问题,最常见的做法是核心的业务逻辑代码中加锁操作,即Synchronied关键字,然而在微服务、分布式系统架构时代,这种做法是行不通的。因为关键字和单一服务节点所在的JVM相关联,而分布式系统下服务一般是部署在不同的节点,从而当高并发请求时候,Synchronied将会力不从心

送一基于Redis的原子操作实现分布式锁,由于Redis底层架构师采用单线程进行涉及到,所以他提供的这些操作也是单线程的,及其操作具备原子性,所谓原子性,指的是同一时刻只能有一个线程处理核心业务逻辑;当其他线程对应的请求时,如果前面的线程没有处理完毕,那么当前线程进入等待状态(阻塞),知道前面的线程处理完毕。

改造后的抢红包

/**
     * 加分布式锁的情况
     * 抢红包-分“点”与“抢”处理逻辑
     * @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());
        }

        //"点红包"
        Boolean res=click(redId);
        if (res){
            //上锁:一个红包每个人只能抢一次随机金额;一个人每次只能抢到红包的一次随机金额  即要永远保证 1对1 的关系
            final String lockKey=redId+userId+"-lock";
            Boolean lock=valueOperations.setIfAbsent(lockKey,redId);
            redisTemplate.expire(lockKey,24L,TimeUnit.HOURS);
            try {
                if (lock) {

                    //"抢红包"-且红包有钱
                    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;
                    }

                }
            }catch (Exception e){
                throw new Exception("系统异常-抢红包-加分布式锁失败!");
            }
        }
        return null;
    }

在这里插入图片描述
在这里插入图片描述

待改进之处

被抢了6个的红包剩下的4个应该归还发红包者

全部相关代码

在这里插入图片描述
在这里插入图片描述

package com.zwx.middleware.dto;

import lombok.Data;
import lombok.ToString;

import javax.validation.constraints.NotNull;


@Data
@ToString
public class RedPacketDto {

    private Integer userId;

    //指定多少人抢
    @NotNull
    private Integer total;

    //指定总金额-单位为分
    @NotNull
    private Integer amount;
}


package com.zwx.middleware.service.redis;

import com.zwx.middleware.dto.RedPacketDto;
import com.zwx.middleware.service.IRedPacketService;
import com.zwx.middleware.service.IRedService;
import com.zwx.middleware.utils.RedPacketUtil;
import com.zwx.middleware.utils.SnowFlake;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Service
@Slf4j
public class RedPacketService implements IRedPacketService {


    private final SnowFlake snowFlake=new SnowFlake(2,3);

    //存储至缓存系统Redis时定义的key
    private static final String keyPrefix="redis:red:packet:";


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IRedService redService;


    /**
     * 发红包
     * @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());

            //生成红包全局唯一标识,并将随机金额、个数入缓存
            String timestamp=String.valueOf(System.nanoTime());
            //根据缓存key的前缀与其他信息拼成一个新的用于存储随机金额列表的key
            String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":").append(timestamp).toString();
            //将随机金额列表存入缓存列表中
            redisTemplate.opsForList().leftPushAll(redId,list);
            //根据缓存key的前缀与其他信息拼成一个新的用于存储红包总数的key
            String redTotalKey = redId+":total";
            //将红包总数存入缓存中
            redisTemplate.opsForValue().set(redTotalKey,dto.getTotal());

            //异步记录红包发出的记录-包括个数与随机金额
            redService.recordRedPacket(dto,redId,list);

            return redId;
        }else{
            throw new Exception("系统异常-分发红包-参数不合法!");
        }
    }

    /**
     * 加分布式锁的情况
     * 抢红包-分“点”与“抢”处理逻辑
     * @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());
        }

        //"点红包"
        Boolean res=click(redId);
        if (res){
            //上锁:一个红包每个人只能抢一次随机金额;一个人每次只能抢到红包的一次随机金额  即要永远保证 1对1 的关系
            final String lockKey=redId+userId+"-lock";
            Boolean lock=valueOperations.setIfAbsent(lockKey,redId);
            redisTemplate.expire(lockKey,24L,TimeUnit.HOURS);
            try {
                if (lock) {

                    //"抢红包"-且红包有钱
                    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;
                    }

                }
            }catch (Exception e){
                throw new Exception("系统异常-抢红包-加分布式锁失败!");
            }
        }
        return null;
    }

    /**
     * 不加分布式锁的情况
     * 抢红包-分“点”与“抢”处理逻辑
     * @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());
//        }
//
//        //"点红包"
//        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;
//    }


    /**
     * 点红包-返回true,则代表红包还有,个数>0
     * @throws Exception
     */
    private Boolean click(String redId) throws Exception{
        ValueOperations valueOperations=redisTemplate.opsForValue();

        String redTotalKey = redId+":total";
        Object total=valueOperations.get(redTotalKey);
        if (total!=null && Integer.valueOf(total.toString())>0){
            return true;
        }
        return false;
    }
}

package com.zwx.middleware.service.redis;

import com.zwx.middleware.dto.RedPacketDto;
import com.zwx.middleware.entity.RedDetail;
import com.zwx.middleware.entity.RedRecord;
import com.zwx.middleware.entity.RedRobRecord;
import com.zwx.middleware.mapper.RedDetailMapper;
import com.zwx.middleware.mapper.RedRecordMapper;
import com.zwx.middleware.mapper.RedRobRecordMapper;
import com.zwx.middleware.service.IRedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;


@Service
@EnableAsync
public class RedService implements IRedService {

    @Resource
    private RedRecordMapper redRecordMapper;

    @Resource
    private RedDetailMapper redDetailMapper;

    @Resource
    private RedRobRecordMapper redRobRecordMapper;


    /**
     * 发红包记录
     * @param dto
     * @param redId
     * @param list
     * @throws Exception
     */
    @Override
    @Async
    @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()));
        redRecordMapper.insertSelective(redRecord);

        RedDetail detail;
        for (Integer i:list){
            detail=new RedDetail();
            detail.setRecordId(redRecord.getId());
            detail.setAmount(BigDecimal.valueOf(i));
            redDetailMapper.insertSelective(detail);
        }
    }

    /**
     * 抢红包记录
     * @param userId
     * @param redId
     * @param amount
     * @throws Exception
     */
    @Override
    @Async
    public void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) throws Exception {
        RedRobRecord redRobRecord=new RedRobRecord();
        redRobRecord.setUserId(userId);
        redRobRecord.setRedPacket(redId);
        redRobRecord.setAmount(amount);
        redRobRecord.setRobTime(new Date());
        redRobRecordMapper.insertSelective(redRobRecord);
    }
}
package com.zwx.middleware.service;


import com.zwx.middleware.dto.RedPacketDto;

import java.math.BigDecimal;


public interface IRedPacketService {

    //发红包核心业务逻辑的实现
    String handOut(RedPacketDto dto) throws Exception;
    //抢红包(在下一节中将实现该方法)
    BigDecimal rob(Integer userId, String redId) throws Exception;
}

package com.zwx.middleware.service;


import com.zwx.middleware.dto.RedPacketDto;

import java.math.BigDecimal;
import java.util.List;

/**
 * 红包记录服务
 */
public interface IRedService {

    //记录发红包时红包的全局唯一标识串、随机金额列表和红包个数等信息入数据库
    void recordRedPacket(RedPacketDto dto, String redId, List<Integer> list) throws Exception;
    //记录抢红包时用户抢到的红包金额等信息入数据库
    void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) throws Exception;

}

package com.zwx.middleware.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 二倍均值法的代码实战
 */
public class RedPacketUtil {

    /**
     * 发红包算法,金额参数以分为单位
     *
     * @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();
            //不断循环遍历、迭代更新地产生随机金额,知道N-1<=0
            for (int i = 0; i < totalPeopleNum - 1; i++) {
                // 随机范围:[1,剩余人均金额的两倍),左闭右开
                //随机金额R、单位为分
                int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
                //更新剩余的总金额M=M-R
                restAmount -= amount;
                //更新剩余的总人数N=N-1
                restPeopleNum--;
                //将产生的随机金额添加进列表中
                amountList.add(amount);
            }
            //循环完毕,剩余的金额为最后一个随机金额,也需要将其添加进列表
            amountList.add(restAmount);
        }

        //最终产生的随机金额返回
        return amountList;
    }

}

在这里插入图片描述

package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedDetail {
    private Integer id; //主键id

    private Integer recordId; //红包记录id

    private BigDecimal amount; //红包随机金额

    private Byte isActive; //是否有效

    private Date createTime; //创建时间


}
package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedRecord {
    private Integer id; //主键id

    private Integer userId; //用户id

    private String redPacket; //红包全局唯一标识串

    private Integer total; //红包指定可以抢的总人数

    private BigDecimal amount; //红包总金额

    private Byte isActive; //是否有效

    private Date createTime; //创建时间


}
package com.zwx.middleware.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class RedRobRecord {
    private Integer id; //主键id

    private Integer userId; //用户id

    private String redPacket; //红包全局唯一标识串

    private BigDecimal amount; //抢到的红包金额

    private Date robTime; //抢到时间

    private Byte isActive; //是否有效

}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedDetail;

public interface RedDetailMapper {

    int deleteByPrimaryKey(Integer id);

    int insert(RedDetail record);

    int insertSelective(RedDetail record);

    RedDetail selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedDetail record);

    int updateByPrimaryKey(RedDetail record);
}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedRecord;

public interface RedRecordMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(RedRecord record);

    int insertSelective(RedRecord record);

    RedRecord selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedRecord record);

    int updateByPrimaryKey(RedRecord record);
}
package com.zwx.middleware.mapper;


import com.zwx.middleware.entity.RedRobRecord;

public interface RedRobRecordMapper {
    int deleteByPrimaryKey(Integer id);

    int insert(RedRobRecord record);

    int insertSelective(RedRobRecord record);

    RedRobRecord selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(RedRobRecord record);

    int updateByPrimaryKey(RedRobRecord record);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedDetailMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedDetail" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="record_id" property="recordId" jdbcType="INTEGER" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, record_id, amount, is_active, create_time
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from red_detail
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_detail
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedDetail" >
    insert into red_detail (id, record_id, amount, 
      is_active, create_time)
    values (#{id,jdbcType=INTEGER}, #{recordId,jdbcType=INTEGER}, #{amount,jdbcType=DECIMAL}, 
      #{isActive,jdbcType=TINYINT}, #{createTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" parameterType="com.zwx.middleware.entity.RedDetail" >
    insert into red_detail
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="recordId != null" >
        record_id,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
      <if test="createTime != null" >
        create_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="recordId != null" >
        #{recordId,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        #{createTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedDetail" >
    update red_detail
    <set >
      <if test="recordId != null" >
        record_id = #{recordId,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedDetail" >
    update red_detail
    set record_id = #{recordId,jdbcType=INTEGER},
      amount = #{amount,jdbcType=DECIMAL},
      is_active = #{isActive,jdbcType=TINYINT},
      create_time = #{createTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedRecordMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedRecord" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="user_id" property="userId" jdbcType="INTEGER" />
    <result column="red_packet" property="redPacket" jdbcType="VARCHAR" />
    <result column="total" property="total" jdbcType="INTEGER" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user_id, red_packet, total, amount, is_active, create_time
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select
    <include refid="Base_Column_List" />
    from red_record
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_record
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedRecord" >
    insert into red_record (id, user_id, red_packet,
      total, amount, is_active,
      create_time)
    values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{redPacket,jdbcType=VARCHAR},
      #{total,jdbcType=INTEGER}, #{amount,jdbcType=DECIMAL}, #{isActive,jdbcType=TINYINT},
      #{createTime,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" parameterType="com.zwx.middleware.entity.RedRecord" >
    insert into red_record
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userId != null" >
        user_id,
      </if>
      <if test="redPacket != null" >
        red_packet,
      </if>
      <if test="total != null" >
        total,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
      <if test="createTime != null" >
        create_time,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userId != null" >
        #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="total != null" >
        #{total,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        #{createTime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedRecord" >
    update red_record
    <set >
      <if test="userId != null" >
        user_id = #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        red_packet = #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="total != null" >
        total = #{total,jdbcType=INTEGER},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
      <if test="createTime != null" >
        create_time = #{createTime,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedRecord" >
    update red_record
    set user_id = #{userId,jdbcType=INTEGER},
      red_packet = #{redPacket,jdbcType=VARCHAR},
      total = #{total,jdbcType=INTEGER},
      amount = #{amount,jdbcType=DECIMAL},
      is_active = #{isActive,jdbcType=TINYINT},
      create_time = #{createTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zwx.middleware.mapper.RedRobRecordMapper" >
  <resultMap id="BaseResultMap" type="com.zwx.middleware.entity.RedRobRecord" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="user_id" property="userId" jdbcType="INTEGER" />
    <result column="red_packet" property="redPacket" jdbcType="VARCHAR" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="rob_time" property="robTime" jdbcType="TIMESTAMP" />
    <result column="is_active" property="isActive" jdbcType="TINYINT" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user_id, red_packet, amount, rob_time, is_active
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from red_rob_record
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from red_rob_record
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    insert into red_rob_record (id, user_id, red_packet, 
      amount, rob_time, is_active
      )
    values (#{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{redPacket,jdbcType=VARCHAR}, 
      #{amount,jdbcType=DECIMAL}, #{robTime,jdbcType=TIMESTAMP}, #{isActive,jdbcType=TINYINT}
      )
  </insert>
  <insert id="insertSelective" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    insert into red_rob_record
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userId != null" >
        user_id,
      </if>
      <if test="redPacket != null" >
        red_packet,
      </if>
      <if test="amount != null" >
        amount,
      </if>
      <if test="robTime != null" >
        rob_time,
      </if>
      <if test="isActive != null" >
        is_active,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userId != null" >
        #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        #{amount,jdbcType=DECIMAL},
      </if>
      <if test="robTime != null" >
        #{robTime,jdbcType=TIMESTAMP},
      </if>
      <if test="isActive != null" >
        #{isActive,jdbcType=TINYINT},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    update red_rob_record
    <set >
      <if test="userId != null" >
        user_id = #{userId,jdbcType=INTEGER},
      </if>
      <if test="redPacket != null" >
        red_packet = #{redPacket,jdbcType=VARCHAR},
      </if>
      <if test="amount != null" >
        amount = #{amount,jdbcType=DECIMAL},
      </if>
      <if test="robTime != null" >
        rob_time = #{robTime,jdbcType=TIMESTAMP},
      </if>
      <if test="isActive != null" >
        is_active = #{isActive,jdbcType=TINYINT},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zwx.middleware.entity.RedRobRecord" >
    update red_rob_record
    set user_id = #{userId,jdbcType=INTEGER},
      red_packet = #{redPacket,jdbcType=VARCHAR},
      amount = #{amount,jdbcType=DECIMAL},
      rob_time = #{robTime,jdbcType=TIMESTAMP},
      is_active = #{isActive,jdbcType=TINYINT}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>

在这里插入图片描述

package com.zwx.middleware.enums;

/**
 * 通用状态码类
 */
public enum StatusCode {
    //以下是暂时设定的几种状态码类
    Success(0,"成功"),
    Fail(-1,"失败"),
    InvalidParams(201,"非法的参数!"),
    InvalidGrantType(202,"非法的授权类型");

    //状态码
    private Integer code;
    //描述信息
    private String msg;
    //重载的构造方法
    StatusCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

package com.zwx.middleware.response;


import com.zwx.middleware.enums.StatusCode;


public class BaseResponse<T> {
    //状态码
    private Integer code;
    //描述信息
    private String msg;
    //响应数据-采用泛型表示可以接受通用的数据类型
    private T data;
    //重载的构造方法一
    public BaseResponse(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    //重载的构造方法二
    public BaseResponse(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }
    //重载的构造方法三
    public BaseResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值