一、Redis应用场景之抢红包系统(二)
1、开发流程介绍
抢红包系统处理用户请求的过程和数据流向如下:

1.1、发红包请求处理
- 后端主要根据用户输入的金额和个数预生成相应的红包随机金额列表,然后将红包的总个数和对应的随机金额列表缓存到Redis中。
- 将红包的总金额、随机金额列表和红包全局唯一标识串信息异步记录到对应的数据库表中。
1.2、抢红包请求处理
- 后端接口先接收到前端用户账号以及红包的全局唯一标识串等请求信息,假设用户账号合法性等信息全部校验通过,然后开始处理用户点开红包的逻辑
- 主要是从Redis中获取当前剩余的红包个数,根据红包个数是否大于0,则开始处理用户拆红包的逻辑。
- 拆红包逻辑主要是从Redis中的随机金额队列中弹出一个红包金额,并根据金额是否为null判断是否成功抢到红包
1.3、状态码设计
保证系统整体开发流程规范,约定处理用户请求信息后将返回统一的响应格式。
BaseResponse类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package com.victor.model.redpacket;
import lombok.Data;
import lombok.ToString;
/**
* @Description: 响应信息类
* @Author: VictorDan
* @Version: 1.0
*/
public class BaseResponse<T> {
/**
* 状态码
*/
private Integer code;
/**
* 描述信息
*/
private String msg;
/**
* 响应数据-采用泛型表示可以接受通用的数据类型
*/
private T data;
public BaseResponse(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public BaseResponse(StatusCode statusCode) {
this.code = statusCode.getCode();
this.msg = statusCode.getMsg();
}
public BaseResponse(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}StatusCode类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50package com.victor.model.redpacket;
import lombok.ToString;
/**
* @Description: 状态码类
* 注:状态码类是枚举类,所以不能使用@Data,需要自己生成getter/setter方法
* @Author: VictorDan
* @Version: 1.0
*/
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 String getMsg() {
return msg;
}
public void setCode(Integer code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
1.4、红包金额随机生成算法
抢红包系统的核心部分主要在于抢红包的逻辑处理,而能否抢到红包,主要取决于红包个数和红包随机金额列表。
红包随机金额列表采用预生成的方式,也就是通过给定红包的总金额M和总人数N,采用随机数算法生成红包随机金额列表,并将它放到Redis中,用来拆分红逻辑的处理
1、随机数算法要求
- 所有人抢到的金额之和等于红包金额
- 每个人至少抢到0.01元
- 要保证所有人抢到金额的记录相等。
采用蒙特卡洛方法,主要构造一个数学模型,将若干个随机变量和统计分析的方法求出若干个随机数。
2、二倍均值法
- 实际中红包随机金额的生成算法有许多,二倍均值法是比较典型。
- 根据每次剩余的总金额M和剩余人数N,执行M/N,然后在乘以2,所得到的数E,在0到E的区间内,随机产生一个随机金额。
- 重复上面的步骤,直到剩余最后一个人。
1 | package com.victor.util; |
2、发红包模拟实战
- 发红包模块的核心处理逻辑在于接受前端发红包设定的总金额M和总个数N,后端接口根据这2个参数,然后采用二倍均值算法生成N个随机金额的红包,最后将红包个数N和随机金额list存到缓存,然后把相关的数据异步记录到数据库中。
- 后端接口在接收到前端用户发红包请求的时候,可以采用时间戳作为红包全局唯一标识串,然后将这个串返回给前端,后续用户发红包的时候,会带上这个参数,目的为了给发出的红包打标记,把这个标记作为key去缓存中查询红包个数和随机金额列表等。
2.1、发红包请求参数RedPacketRequest
1 | package com.victor.model.redpacket; |
2.2、发红包的控制器RedPacketController
1 | package com.victor.controller; |
2.3、发红包的核心逻辑Service
- 发红包接口IRedPacketService
1 | package com.victor.service.redpacket; |
发红包核心逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68package com.victor.service.redpacket.impl;
import com.victor.model.redpacket.RedPacketRequest;
import com.victor.service.redpacket.IRedPacketService;
import com.victor.service.redpacket.IRedService;
import com.victor.util.RedPacketUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
/**
* @Description:
* @Author: VictorDan
* @Version: 1.0
*/
4j
public class RedPacketServiceImpl implements IRedPacketService {
private RedisTemplate redisTemplate;
/**
* 红包业务逻辑处理过程记录到数据库的服务
*/
private IRedService redService;
//存储到Redis中定义key前缀
private static final String KEY_PREFIX = "redis:red:packet:";
/**
* 发红包
*
* @param request
* @return
* @throws Exception
*/
public String handOut(RedPacketRequest request) throws Exception {
if (request.getTotal() > 0 && request.getTotal() > 0) {
List<Integer> list = RedPacketUtil.divideRedPacket(request.getAmount(), request.getTotal());
//生成红包全局唯一标识串
String timestamp = String.valueOf(System.nanoTime());
//根据缓存key的前缀与其他信息拼接成一个新的用来存储红包总数的key
String redId = KEY_PREFIX + request.getUserId() + ":" + timestamp;
//将随机金额写入到redis的list中(从队列左边全部push进去)
redisTemplate.opsForList().leftPushAll(redId, list);
//将红包总个数写入redis中
String redTotalKey = redId + ":total";
redisTemplate.opsForValue().set(redTotalKey, request.getTotal());
//异步记录红包的全局标识符,红包个数与随机金额列表到数据库
redService.recordRedPacket(request, redId, list);
//返回红包的全局唯一标识符给前端
return redId;
} else {
throw new Exception("系统异常--->分发红包参数不合法");
}
}
public BigDecimal rob(Integer userId, String redId) {
return null;
}
}
2.4、异步保存红包业务逻辑处理过程数据到数据库
红包业务逻辑处理过程数据接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.victor.service.redpacket;
import com.victor.model.redpacket.RedPacketRequest;
import java.util.List;
/**
* @Description: 红包业务逻辑处理过程数据记录
* @Author: VictorDan
* @Version: 1.0
*/
public interface IRedService {
/**
* 记录发红包
* @param dto
* @param redId
* @param list
*/
void recordRedPacket(RedPacketRequest dto, String redId, List<Integer> list);
}红包业务逻辑处理过程数据实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70package com.victor.service.redpacket.impl;
import com.victor.model.redpacket.RedPacketRequest;
import com.victor.model.redpacket.RedDetail;
import com.victor.model.redpacket.RedRecord;
import com.victor.repository.redpacket.RedDetailRepo;
import com.victor.repository.redpacket.RedRecordRepo;
import com.victor.repository.redpacket.RedRobRecordRepo;
import com.victor.service.redpacket.IRedService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* @Description: 红包业务逻辑处理过程数据记录
* @Author: VictorDan
* @Version: 1.0
*/
4j
public class RedServiceImpl implements IRedService {
private RedRecordRepo redRecordRepo;
private RedDetailRepo redDetailRepo;
private RedRobRecordRepo redRobRecordRepo;
/**
* 发红包记录-异步方式
* @param dto 红包总金额+个数
* @param redId 红包全局唯一标识串
* @param list 红包随机金额列表
*/
(rollbackFor = Exception.class)
@Async
@Override
public void recordRedPacket(RedPacketRequest dto, String redId, List<Integer> list){
RedRecord redRecord = new RedRecord();
redRecord.setUserId(dto.getUserId());
redRecord.setAmount(BigDecimal.valueOf(dto.getAmount()));
redRecord.setTotal(dto.getTotal());
redRecord.setRedPacket(redId);
redRecord.setIsActive(true);
redRecord.setCreateTime(new Date());
//保存到发红包记录表
redRecordRepo.save(redRecord);
RedDetail redDetail;
//遍历随机金额列表,将金额等信息设置到对应字段
for (Integer data:list) {
redDetail = new RedDetail();
redDetail.setRecordId(redRecord.getId());
redDetail.setAmount(BigDecimal.valueOf(data));
redDetail.setCreateTime(new Date());
redDetail.setIsActive(true);
//保存红包明细金额表
redDetailRepo.saveAndFlush(redDetail);
}
}
}
2.5、测试结果
使用PostMan测试接口结果如下:

查看数据库记录
1
2
3
4#查看发红包表
SELECT * FROM victor.red_record;
#查看红包明细表,是否对应总金额
SELECT sum(amount) FROM victor.red_detail where record_id=1 and is_active=1;- 查看发红包表

查看红包明细表

汇总红包明细表的金额与总金额对比

- 本文作者: Victor Dan
- 本文链接: https://victorblog.github.io/2018/06/19/Redis应用场景之抢红包系统-二/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
