Java案例如何实现红包算法?

wen java案例 5

本文目录导读:

Java案例如何实现红包算法?

  1. 二倍均值法(常用算法)
  2. 完整版红包服务
  3. 高并发抢红包方案
  4. 简单测试类
  5. 算法特点说明

二倍均值法(常用算法)

这是微信红包使用的经典算法,保证每个人都能分到钱且相对公平。

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);
        }
    }
}

算法特点说明

  1. 二倍均值法

    • 公平性:每人抢到的金额随机
    • 期望值:每个人获得金额的期望值相同
    • 不会出现金额为0的情况
  2. 注意事项

    • 使用分作为单位避免浮点数精度问题
    • 高并发时需考虑线程安全
    • 实际生产环境建议使用Redis等中间件
  3. 扩展功能

    • 可以增加红包过期自动退款
    • 可以记录抢红包日志
    • 可以实现红包查看记录功能

这个实现可以满足大多数红包业务场景的需求。

抱歉,评论功能暂时关闭!