方案一:数据库核心方案
1. 数据库表设计
sql
-- 红包表
CREATE TABLE red_packet (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
packet_id VARCHAR(64) NOT NULL UNIQUE COMMENT '红包ID',
user_id BIGINT NOT NULL COMMENT '发红包用户ID',
total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
total_count INT NOT NULL COMMENT '总个数',
remaining_amount DECIMAL(10,2) NOT NULL COMMENT '剩余金额',
remaining_count INT NOT NULL COMMENT '剩余个数',
type TINYINT NOT NULL COMMENT '红包类型:1-普通,2-拼手气',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-有效,2-过期,3-已抢完',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_packet_id (packet_id),
INDEX idx_status_create_time (status, create_time)
);
-- 红包领取记录表
CREATE TABLE red_packet_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
packet_id VARCHAR(64) NOT NULL COMMENT '红包ID',
user_id BIGINT NOT NULL COMMENT '领取用户ID',
amount DECIMAL(10,2) NOT NULL COMMENT '领取金额',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_packet_user (packet_id, user_id),
INDEX idx_user_id (user_id),
INDEX idx_packet_id (packet_id)
);
2. 实体类
java
@Data
public class RedPacket {
private Long id;
private String packetId;
private Long userId;
private BigDecimal totalAmount;
private Integer totalCount;
private BigDecimal remainingAmount;
private Integer remainingCount;
private Integer type;
private Integer status;
private Date createTime;
private Date updateTime;
}
@Data
public class RedPacketRecord {
private Long id;
private String packetId;
private Long userId;
private BigDecimal amount;
private Date createTime;
}
3. 红包服务实现
java
@Service
@Slf4j
public class RedPacketService {
@Autowired
private RedPacketMapper redPacketMapper;
@Autowired
private RedPacketRecordMapper redPacketRecordMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String RED_PACKET_PREFIX = "red_packet:";
private static final String RED_PACKET_LOCK_PREFIX = "red_packet_lock:";
/**
* 发红包
*/
@Transactional
public String sendRedPacket(Long userId, BigDecimal totalAmount, Integer totalCount, Integer type) {
// 参数校验
if (totalAmount.compareTo(BigDecimal.ZERO) <= 0 || totalCount <= 0) {
throw new BusinessException("红包金额和个数必须大于0");
}
if (type == 1 && totalAmount.divide(BigDecimal.valueOf(totalCount), 2, RoundingMode.DOWN)
.compareTo(BigDecimal.valueOf(0.01)) < 0) {
throw new BusinessException("普通红包每人至少0.01元");
}
// 生成红包ID
String packetId = generatePacketId();
// 创建红包
RedPacket redPacket = new RedPacket();
redPacket.setPacketId(packetId);
redPacket.setUserId(userId);
redPacket.setTotalAmount(totalAmount);
redPacket.setTotalCount(totalCount);
redPacket.setRemainingAmount(totalAmount);
redPacket.setRemainingCount(totalCount);
redPacket.setType(type);
redPacket.setStatus(1);
redPacketMapper.insert(redPacket);
// 预生成红包金额(针对拼手气红包)
if (type == 2) {
List<BigDecimal> amounts = generateRandomAmounts(totalAmount, totalCount);
// 存储到Redis
redisTemplate.opsForList().rightPushAll(RED_PACKET_PREFIX + packetId,
amounts.toArray());
}
log.info("用户{}发放红包{}, 金额:{}, 个数:{}", userId, packetId, totalAmount, totalCount);
return packetId;
}
/**
* 抢红包
*/
@Transactional
public BigDecimal grabRedPacket(String packetId, Long userId) {
// 检查是否已经抢过
RedPacketRecord existingRecord = redPacketRecordMapper.selectByPacketIdAndUserId(packetId, userId);
if (existingRecord != null) {
return existingRecord.getAmount();
}
// 获取分布式锁
String lockKey = RED_PACKET_LOCK_PREFIX + packetId;
boolean locked = false;
try {
locked = tryLock(lockKey, 30);
if (!locked) {
throw new BusinessException("抢红包过于频繁,请稍后重试");
}
// 查询红包信息
RedPacket redPacket = redPacketMapper.selectByPacketId(packetId);
if (redPacket == null || redPacket.getStatus() != 1) {
throw new BusinessException("红包不存在或已失效");
}
if (redPacket.getRemainingCount() <= 0) {
throw new BusinessException("红包已抢完");
}
BigDecimal grabAmount;
if (redPacket.getType() == 1) {
// 普通红包
grabAmount = calculateNormalAmount(redPacket);
} else {
// 拼手气红包
grabAmount = calculateRandomAmount(redPacket);
}
// 更新红包信息
int updateCount = redPacketMapper.updateRemaining(
packetId,
redPacket.getRemainingAmount().subtract(grabAmount),
redPacket.getRemainingCount() - 1
);
if (updateCount == 0) {
throw new BusinessException("红包已抢完");
}
// 记录领取记录
RedPacketRecord record = new RedPacketRecord();
record.setPacketId(packetId);
record.setUserId(userId);
record.setAmount(grabAmount);
redPacketRecordMapper.insert(record);
log.info("用户{}抢到红包{}, 金额:{}", userId, packetId, grabAmount);
return grabAmount;
} finally {
if (locked) {
unlock(lockKey);
}
}
}
/**
* 生成随机红包金额(二倍均值法)
*/
private List<BigDecimal> generateRandomAmounts(BigDecimal totalAmount, Integer totalCount) {
List<BigDecimal> amounts = new ArrayList<>();
BigDecimal remainingAmount = totalAmount;
int remainingCount = totalCount;
Random random = new Random();
for (int i = 0; i < totalCount - 1; i++) {
// 最大金额 = 剩余金额 / 剩余个数 * 2
BigDecimal max = remainingAmount.divide(
BigDecimal.valueOf(remainingCount), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(2));
BigDecimal amount = BigDecimal.valueOf(random.nextDouble())
.multiply(max)
.setScale(2, RoundingMode.HALF_UP);
// 确保最小金额为0.01
amount = amount.max(BigDecimal.valueOf(0.01));
// 确保不会超过剩余金额
amount = amount.min(remainingAmount.subtract(
BigDecimal.valueOf(0.01).multiply(BigDecimal.valueOf(remainingCount - 1))));
amounts.add(amount);
remainingAmount = remainingAmount.subtract(amount);
remainingCount--;
}
// 最后一个红包
amounts.add(remainingAmount.setScale(2, RoundingMode.HALF_UP));
// 打乱顺序
Collections.shuffle(amounts);
return amounts;
}
/**
* 计算普通红包金额
*/
private BigDecimal calculateNormalAmount(RedPacket redPacket) {
return redPacket.getRemainingAmount()
.divide(BigDecimal.valueOf(redPacket.getRemainingCount()), 2, RoundingMode.HALF_UP);
}
/**
* 计算随机红包金额
*/
private BigDecimal calculateRandomAmount(RedPacket redPacket) {
// 从Redis中获取预生成的红包金额
Object amountObj = redisTemplate.opsForList().leftPop(RED_PACKET_PREFIX + redPacket.getPacketId());
if (amountObj != null) {
return new BigDecimal(amountObj.toString());
}
// 如果Redis中没有,则实时计算(降级方案)
return calculateRandomAmountRealTime(redPacket);
}
/**
* 实时计算随机红包金额
*/
private BigDecimal calculateRandomAmountRealTime(RedPacket redPacket) {
if (redPacket.getRemainingCount() == 1) {
return redPacket.getRemainingAmount();
}
Random random = new Random();
// 最大金额 = 剩余金额 / 剩余个数 * 2
BigDecimal max = redPacket.getRemainingAmount()
.divide(BigDecimal.valueOf(redPacket.getRemainingCount()), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(2));
BigDecimal amount = BigDecimal.valueOf(random.nextDouble())
.multiply(max)
.setScale(2, RoundingMode.HALF_UP);
// 确保最小金额为0.01,且给后面的人留够钱
BigDecimal minAmount = BigDecimal.valueOf(0.01);
BigDecimal maxAmount = redPacket.getRemainingAmount()
.subtract(BigDecimal.valueOf(0.01).multiply(
BigDecimal.valueOf(redPacket.getRemainingCount() - 1)));
amount = amount.max(minAmount).min(maxAmount);
return amount;
}
private String generatePacketId() {
return UUID.randomUUID().toString().replace("-", "");
}
private boolean tryLock(String key, long expireSeconds) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1",
Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(result);
}
private void unlock(String key) {
redisTemplate.delete(key);
}
}
方案二:纯Redis实现方案
java
@Service
@Slf4j
public class RedisRedPacketService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedPacketRecordMapper redPacketRecordMapper;
private static final String RED_PACKET_PREFIX = "red_packet:";
private static final String RED_PACKET_AMOUNTS_PREFIX = "red_packet_amounts:";
private static final String RED_PACKET_RECORDS_PREFIX = "red_packet_records:";
/**
* 发红包(Redis版本)
*/
public String sendRedPacket(Long userId, BigDecimal totalAmount, Integer totalCount, Integer type) {
String packetId = generatePacketId();
// 存储红包基本信息到Redis
Map<String, Object> packetInfo = new HashMap<>();
packetInfo.put("userId", userId.toString());
packetInfo.put("totalAmount", totalAmount.toString());
packetInfo.put("totalCount", totalCount.toString());
packetInfo.put("remainingAmount", totalAmount.toString());
packetInfo.put("remainingCount", totalCount.toString());
packetInfo.put("type", type.toString());
packetInfo.put("status", "1");
packetInfo.put("createTime", System.currentTimeMillis());
redisTemplate.opsForHash().putAll(RED_PACKET_PREFIX + packetId, packetInfo);
// 设置过期时间(24小时)
redisTemplate.expire(RED_PACKET_PREFIX + packetId, Duration.ofHours(24));
// 预生成红包金额
if (type == 2) {
List<BigDecimal> amounts = generateRandomAmounts(totalAmount, totalCount);
String[] amountStrs = amounts.stream()
.map(BigDecimal::toString)
.toArray(String[]::new);
redisTemplate.opsForList().rightPushAll(RED_PACKET_AMOUNTS_PREFIX + packetId, amountStrs);
redisTemplate.expire(RED_PACKET_AMOUNTS_PREFIX + packetId, Duration.ofHours(24));
}
return packetId;
}
/**
* 抢红包(Redis版本)
*/
public BigDecimal grabRedPacket(String packetId, Long userId) {
String userKey = userId.toString();
// 使用Lua脚本保证原子性
String luaScript = """
local packetKey = KEYS[1]
local amountsKey = KEYS[2]
local recordsKey = KEYS[3]
local userKey = ARGV[1]
-- 检查是否已经抢过
if redis.call('HEXISTS', recordsKey, userKey) == 1 then
return redis.call('HGET', recordsKey, userKey)
end
-- 检查红包状态
local remainingCount = tonumber(redis.call('HGET', packetKey, 'remainingCount'))
if not remainingCount or remainingCount <= 0 then
return '0'
end
local packetType = tonumber(redis.call('HGET', packetKey, 'type'))
local amount = '0'
if packetType == 1 then
-- 普通红包
local remainingAmount = tonumber(redis.call('HGET', packetKey, 'remainingAmount'))
amount = tostring(remainingAmount / remainingCount)
else
-- 拼手气红包
amount = redis.call('LPOP', amountsKey)
if not amount then
-- 实时计算
local remainingAmount = tonumber(redis.call('HGET', packetKey, 'remainingAmount'))
if remainingCount == 1 then
amount = tostring(remainingAmount)
else
local max = (remainingAmount / remainingCount) * 2
math.randomseed(tonumber(tostring(remainingAmount):reverse():sub(1,6)))
amount = tostring(math.random() * max)
amount = string.format('%.2f', math.max(0.01, math.min(tonumber(amount),
remainingAmount - 0.01 * (remainingCount - 1))))
end
end
end
if amount == '0' then
return '0'
end
-- 更新红包信息
local newRemainingCount = remainingCount - 1
local newRemainingAmount = tonumber(redis.call('HGET', packetKey, 'remainingAmount')) - tonumber(amount)
redis.call('HSET', packetKey, 'remainingCount', newRemainingCount)
redis.call('HSET', packetKey, 'remainingAmount', string.format('%.2f', newRemainingAmount))
-- 记录领取记录
redis.call('HSET', recordsKey, userKey, amount)
return amount
""";
RedisScript<String> script = RedisScript.of(luaScript, String.class);
List<String> keys = Arrays.asList(
RED_PACKET_PREFIX + packetId,
RED_PACKET_AMOUNTS_PREFIX + packetId,
RED_PACKET_RECORDS_PREFIX + packetId
);
String result = redisTemplate.execute(script, keys, userKey);
if ("0".equals(result)) {
throw new BusinessException("红包已抢完");
}
BigDecimal amount = new BigDecimal(result);
// 异步保存到数据库
saveRecordAsync(packetId, userId, amount);
return amount;
}
@Async
public void saveRecordAsync(String packetId, Long userId, BigDecimal amount) {
try {
RedPacketRecord record = new RedPacketRecord();
record.setPacketId(packetId);
record.setUserId(userId);
record.setAmount(amount);
redPacketRecordMapper.insert(record);
} catch (Exception e) {
log.error("保存红包记录失败: packetId={}, userId={}", packetId, userId, e);
}
}
}
方案三:防刷和限流策略
java
@Service
public class RedPacketSecurityService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String GRAB_LIMIT_PREFIX = "grab_limit:";
private static final String USER_DAILY_LIMIT_PREFIX = "user_daily_limit:";
/**
* 检查用户抢红包频率
*/
public boolean checkGrabFrequency(Long userId, String packetId) {
String key = GRAB_LIMIT_PREFIX + userId;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, Duration.ofSeconds(10));
}
return count <= 5; // 10秒内最多抢5次
}
/**
* 检查用户每日抢红包上限
*/
public boolean checkDailyLimit(Long userId) {
String key = USER_DAILY_LIMIT_PREFIX + userId + ":" + LocalDate.now();
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, Duration.ofDays(1));
}
return count <= 100; // 每天最多抢100个红包
}
/**
* 检查红包金额限制
*/
public boolean validateAmount(BigDecimal amount, Integer type) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return false;
}
if (type == 1) {
// 普通红包单笔金额限制
return amount.compareTo(BigDecimal.valueOf(200)) <= 0;
} else {
// 拼手气红包单笔金额限制
return amount.compareTo(BigDecimal.valueOf(200)) <= 0;
}
}
}
方案四:Controller层实现
java
@RestController
@RequestMapping("/api/redpacket")
@Validated
@Slf4j
public class RedPacketController {
@Autowired
private RedPacketService redPacketService;
@Autowired
private RedPacketSecurityService securityService;
/**
* 发红包
*/
@PostMapping("/send")
public ApiResult<String> sendRedPacket(@RequestBody @Valid SendRedPacketRequest request) {
if (!securityService.validateAmount(request.getTotalAmount(), request.getType())) {
return ApiResult.error("红包金额不合法");
}
String packetId = redPacketService.sendRedPacket(
request.getUserId(),
request.getTotalAmount(),
request.getTotalCount(),
request.getType()
);
return ApiResult.success(packetId);
}
/**
* 抢红包
*/
@PostMapping("/grab")
public ApiResult<BigDecimal> grabRedPacket(@RequestBody @Valid GrabRedPacketRequest request) {
// 频率限制检查
if (!securityService.checkGrabFrequency(request.getUserId(), request.getPacketId())) {
return ApiResult.error("操作过于频繁,请稍后重试");
}
// 每日上限检查
if (!securityService.checkDailyLimit(request.getUserId())) {
return ApiResult.error("今日抢红包次数已达上限");
}
try {
BigDecimal amount = redPacketService.grabRedPacket(request.getPacketId(), request.getUserId());
return ApiResult.success(amount);
} catch (BusinessException e) {
return ApiResult.error(e.getMessage());
}
}
/**
* 查询红包详情
*/
@GetMapping("/detail/{packetId}")
public ApiResult<RedPacketDetailVO> getRedPacketDetail(@PathVariable String packetId) {
RedPacketDetailVO detail = redPacketService.getRedPacketDetail(packetId);
return ApiResult.success(detail);
}
}
@Data
class SendRedPacketRequest {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotNull(message = "总金额不能为空")
@DecimalMin(value = "0.01", message = "金额必须大于0")
private BigDecimal totalAmount;
@NotNull(message = "红包个数不能为空")
@Min(value = 1, message = "红包个数必须大于0")
private Integer totalCount;
@NotNull(message = "红包类型不能为空")
@Range(min = 1, max = 2, message = "红包类型不合法")
private Integer type;
}
@Data
class GrabRedPacketRequest {
@NotBlank(message = "红包ID不能为空")
private String packetId;
@NotNull(message = "用户ID不能为空")
private Long userId;
}
总结
这个红包系统实现方案包含以下特点:
-
高并发处理:使用Redis和分布式锁保证并发安全
-
多种红包类型:支持普通红包和拼手气红包
-
防刷策略:频率限制和每日上限
-
原子性操作:使用Lua脚本保证Redis操作的原子性
-
降级方案:当预生成金额用尽时实时计算
-
数据一致性:Redis缓存 + 数据库持久化