采用二分法划分红包
public static List<Integer> divideRedPackage(Integer totalAmount,Integer totalPeopleNum){
int restAmount=totalAmount; //剩余金额
int restPeopleNum=totalPeopleNum; //剩余人数
ArrayList<Integer>amountList= new ArrayList<>(); //存储红包金额
Random rand = new Random();
for(;restPeopleNum>1;restPeopleNum--){
// restAmount/restPeopleNum>=1
int amount=rand.nextInt(restAmount/restPeopleNum*2-1)+1;
amountList.add(amount);
restAmount-=amount;
}
amountList.add(restAmount);
return amountList;
}
数据库表对应的model层
RedRecord:
private Integer id; //主键
private Integer userId; //用户id
private String redPacket; //红包标识串
private Integer total; //红包数量
private BigDecimal amount; //红包总金额(单位 分)
private Byte isActive; //是否有效
private Date createTime; //创建时间
RedDetail: 记录单个小红包信息
private Integer id; //主键
private Integer recordId; //对应的红包主键id
private BigDecimal amount; //单个小红包金额
private Byte isActive;
private Date createTime
RedRobRecord
private Integer id;
private Integer userId; //抢红包的user的id
private String redPacket; //红包标识串
private BigDecimal amount; //小红包的金额
private Date robTime;
private Byte isActive;
控制层
@RestController
public class RedPacketController {
private static final Logger log = getLogger(RedPacketController.class);
private static final String prefix="red/packet";
@Autowired
private IRedPacketService redPacketService;
@PostMapping(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.Succuss);
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;
}
@GetMapping(prefix+"/rob")
public BaseResponse rob(@RequestParam Integer userId,@RequestParam String redId){
BaseResponse response = new BaseResponse(StatusCode.Succuss);
try {
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;
}
}
服务层
@Service
public class RedPacketService implements IRedPacketService {
private static final Logger log = getLogger(RedPacketService.class);
private static final String keyPrefix = "redis:red:packet:";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IRedService redService;
@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()); //随机数字串
String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":")
.append(timestamp).toString(); //红包唯一标识
redisTemplate.opsForList().leftPushAll(redId, list);
String redTotalKey = redId + ":total";
redisTemplate.opsForValue().set(redTotalKey, dto.getTotal());
redService.recordRedPacket(dto, redId, list);
return redId; //通过redId->list
} else {
throw new 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) {
final String lockKey = redId + userId + "-lock";
//间接实现分布式锁
Boolean lock = valueOperations.setIfAbsent(lockKey, redId);
redisTemplate.expire(lockKey, 24L, TimeUnit.HOURS);
//从红包列表中弹出一个值
Object value = redisTemplate.opsForList().rightPop(redId);
if (value != null) {
try {
if (lock) { //加锁,防止高并发下同一个用户多次抢红包
String redTotalKey = redId + ":total";
Integer currTotal = valueOperations.get(redTotalKey) != null ? (Integer) valueOperations.get(redTotalKey) : 0;
valueOperations.set(redTotalKey, currTotal - 1); //更新红包个数
//红包单位:分换算为元 除以100
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;
}
private Boolean click(String redId) {
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;
}
}
@Service
@EnableAsync
public class RedService implements IRedService {
private static final Logger log = getLogger(RedService.class);
@Autowired
RedRecordMapper redRecordMapper;
@Autowired
RedDetailMapper redDetailMapper;
@Autowired
RedRobRecordMapper redRobRecordMapper;
@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.setAmount(BigDecimal.valueOf(dto.getAmount()));
redRecord.setTotal(dto.getTotal());
redRecord.setRedPacket(redId);
redRecord.setCreateTime(new Date());
redRecordMapper.insertSelective(redRecord);
RedDetail redDetail;
for (Integer i : list) {
redDetail =new RedDetail();
redDetail.setAmount(BigDecimal.valueOf(i));
redDetail.setRecordId(redRecord.getId());
redDetail.setCreateTime(new Date());
redDetailMapper.insertSelective(redDetail);
}
}
@Override
@Async
public void recordRobRedPacket(Integer userId, String redId, BigDecimal amount) throws Exception {
RedRobRecord redRobRecord = new RedRobRecord();
redRobRecord.setAmount(amount);
redRobRecord.setRedPacket(redId);
redRobRecord.setUserId(userId);
redRobRecord.setRobTime(new Date());
redRobRecordMapper.insertSelective(redRobRecord);
}
}
Dto层
public class RedPacketDto {
private Integer userId;
@NotNull
private Integer total; //红包个数
@NotNull
private Integer amount; //红包总金额
}
entity
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 enum StatusCode {
Succuss(0,"成功"),
Fail(-1,"失败"),
InvalidParams(-2,"参数非法");
private Integer code;
private String msg;
StatusCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
流程图