本文目录导读:

二倍均值法(常用算法)
这是微信红包使用的经典算法,保证每个人都能分到钱且相对公平。
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class RedPacketUtils {
/**
* 二倍均值法发红包
* @param totalAmount 总金额(元)
* @param totalNum 红包个数
* @return 每个红包的金额列表
*/
public static List<Double> splitRedPacket(double totalAmount, int totalNum) {
List<Double> amountList = new ArrayList<>();
// 将元转换为分,避免浮点数精度问题
int totalCent = (int) (totalAmount * 100);
int remainAmount = totalCent;
int remainNum = totalNum;
Random random = new Random();
for (int i = 0; i < totalNum - 1; i++) {
// 每个人获得的红包金额是 [0.01, 剩余平均值*2] 之间的随机数
int maxAmount = remainAmount / remainNum * 2;
int amount = random.nextInt(maxAmount) + 1; // 至少1分
remainAmount -= amount;
remainNum--;
amountList.add(amount / 100.0);
}
// 最后一个人获得剩余金额
amountList.add(remainAmount / 100.0);
return amountList;
}
public static void main(String[] args) {
double totalAmount = 10.00;
int totalNum = 5;
List<Double> packets = splitRedPacket(totalAmount, totalNum);
System.out.println("红包分配结果:");
double sum = 0;
for (int i = 0; i < packets.size(); i++) {
System.out.printf("第%d个红包:%.2f元%n", i+1, packets.get(i));
sum += packets.get(i);
}
System.out.printf("总金额:%.2f元%n", sum);
}
}
完整版红包服务
包含接口定义、实体类和业务逻辑:
// 红包结果实体类
public class RedPacketResult {
private Long userId;
private Double amount;
private Long timestamp;
// 构造器、getter、setter省略
}
// 红包服务接口
public interface RedPacketService {
/**
* 发红包
*/
RedPacket sendRedPacket(Long userId, double totalAmount, int totalNum);
/**
* 抢红包
*/
RedPacketResult grabRedPacket(Long redPacketId, Long userId);
}
// 红包服务实现
@Service
public class RedPacketServiceImpl implements RedPacketService {
private static final double MIN_AMOUNT = 0.01;
// 模拟存储
private Map<Long, RedPacket> redPacketMap = new ConcurrentHashMap<>();
private AtomicLong idGenerator = new AtomicLong(1);
@Override
public RedPacket sendRedPacket(Long userId, double totalAmount, int totalNum) {
// 参数校验
if (totalAmount < totalNum * MIN_AMOUNT) {
throw new IllegalArgumentException("金额不足以分配");
}
RedPacket redPacket = new RedPacket();
redPacket.setId(idGenerator.getAndIncrement());
redPacket.setUserId(userId);
redPacket.setTotalAmount(totalAmount);
redPacket.setTotalNum(totalNum);
redPacket.setRemainAmount(totalAmount);
redPacket.setRemainNum(totalNum);
redPacket.setStatus(RedPacketStatus.ACTIVE);
redPacket.setCreateTime(new Date());
// 预分配红包金额(二倍均值法)
redPacket.setAmountList(splitRedPacket(totalAmount, totalNum));
redPacketMap.put(redPacket.getId(), redPacket);
return redPacket;
}
@Override
public RedPacketResult grabRedPacket(Long redPacketId, Long userId) {
RedPacket redPacket = redPacketMap.get(redPacketId);
if (redPacket == null) {
throw new RuntimeException("红包不存在");
}
synchronized (redPacket.getId().toString().intern()) {
if (redPacket.getRemainNum() <= 0) {
throw new RuntimeException("红包已抢完");
}
// 获取当前用户应得的金额
int index = redPacket.getTotalNum() - redPacket.getRemainNum();
Double amount = redPacket.getAmountList().get(index);
// 更新剩余信息
redPacket.setRemainAmount(redPacket.getRemainAmount() - amount);
redPacket.setRemainNum(redPacket.getRemainNum() - 1);
RedPacketResult result = new RedPacketResult();
result.setUserId(userId);
result.setAmount(amount);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
private List<Double> splitRedPacket(double totalAmount, int totalNum) {
// 使用之前的二倍均值法
return RedPacketUtils.splitRedPacket(totalAmount, totalNum);
}
}
高并发抢红包方案
使用Redis实现高并发红包系统:
@Service
public class RedisRedPacketService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String RED_PACKET_KEY = "red_packet:";
private static final String RED_PACKET_CONSUME_KEY = "red_packet:consume:";
/**
* 发红包(Redis实现)
*/
public String sendRedPacket(double totalAmount, int totalNum) {
// 生成红包ID
String redPacketId = UUID.randomUUID().toString().replace("-", "");
// 将金额转换为分存储
int totalCent = (int) (totalAmount * 100);
// 预生成红包金额列表
List<Integer> amountList = new ArrayList<>();
int remainAmount = totalCent;
int remainNum = totalNum;
Random random = new Random();
for (int i = 0; i < totalNum - 1; i++) {
int max = remainAmount / remainNum * 2;
int amount = random.nextInt(max) + 1;
amountList.add(amount);
remainAmount -= amount;
remainNum--;
}
amountList.add(remainAmount);
// 将红包金额列表打乱后存入Redis List
Collections.shuffle(amountList);
String key = RED_PACKET_KEY + redPacketId;
for (Integer amount : amountList) {
redisTemplate.opsForList().rightPush(key, amount);
}
// 设置过期时间(24小时)
redisTemplate.expire(key, 24, TimeUnit.HOURS);
return redPacketId;
}
/**
* 抢红包(Redis实现,支持高并发)
*/
public Double grabRedPacket(String redPacketId, Long userId) {
String key = RED_PACKET_KEY + redPacketId;
String consumeKey = RED_PACKET_CONSUME_KEY + redPacketId;
// 使用Lua脚本保证原子性
String luaScript =
"local amount = redis.call('lpop', KEYS[1]) " +
"if amount then " +
" redis.call('hset', KEYS[2], ARGV[1], amount) " +
" return amount " +
"end " +
"return nil";
Long amount = redisTemplate.execute(
(RedisCallback<Long>) connection -> {
byte[][] keys = new byte[][]{
key.getBytes(),
consumeKey.getBytes()
};
byte[][] argv = new byte[][]{
userId.toString().getBytes()
};
return (Long) connection.eval(
luaScript.getBytes(),
ReturnType.INTEGER,
2,
keys,
argv
);
}
);
if (amount == null) {
return null;
}
return amount / 100.0;
}
}
简单测试类
public class RedPacketTest {
public static void main(String[] args) {
// 测试二倍均值法
System.out.println("===== 测试红包分配 =====");
double total = 100;
int num = 10;
List<Double> amounts = RedPacketUtils.splitRedPacket(total, num);
double sum = 0;
for (int i = 0; i < amounts.size(); i++) {
System.out.printf("用户%d: %.2f元%n", i+1, amounts.get(i));
sum += amounts.get(i);
}
System.out.printf("合计: %.2f元%n", sum);
// 多次测试看分布
System.out.println("\n===== 多次测试统计 =====");
for (int j = 0; j < 5; j++) {
amounts = RedPacketUtils.splitRedPacket(total, num);
double max = amounts.stream().mapToDouble(Double::doubleValue).max().getAsDouble();
double min = amounts.stream().mapToDouble(Double::doubleValue).min().getAsDouble();
System.out.printf("第%d次 - 最大:%.2f, 最小:%.2f%n", j+1, max, min);
}
}
}
算法特点说明
-
二倍均值法:
- 公平性:每人抢到的金额随机
- 期望值:每个人获得金额的期望值相同
- 不会出现金额为0的情况
-
注意事项:
- 使用分作为单位避免浮点数精度问题
- 高并发时需考虑线程安全
- 实际生产环境建议使用Redis等中间件
-
扩展功能:
- 可以增加红包过期自动退款
- 可以记录抢红包日志
- 可以实现红包查看记录功能
这个实现可以满足大多数红包业务场景的需求。