首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 實作微信隨機紅包演算法:二倍均值法完整解析

     編輯:Java 相關知識

前言

微信紅包的核心演算法業界公認採用**「二倍均值法」**。本文深度解析其數學原理、Java 完整實作、邊界條件處理、公平性驗證與擴展優化方案。

一、演算法核心原理:二倍均值法

數學模型

設剩餘總金額為 M,剩餘紅包數為 N,則**當前平均值** avg = M / N。

下一個紅包金額 X 在區間 (0, 2 * avg) 內**均勻隨機**取值:

X ~ Uniform(0, 2 * avg)

為什麼是 2 倍均值?

  • **期望值保證公平**:E[X] = avg,長期期望下每人分得金額相等。
  • **變異數創造驚喜**:標準差 σ = avg / √3 ≈ 0.577 * avg,適度波動製造「手氣最佳」樂趣。
  • **避免極端值**:上限 2*avg 確保單包不會佔據過多餘額,留給後續參與者機會。
  • **動態調整**:每發一包,M 和 N 更新,avg 自動重算,適應剩餘狀態。

最後一包特殊處理

當 N = 1 時,直接分配剩餘所有金額 M,確保總額精確吻合 (解決浮點誤差累積)。

二、生產級 Java 完整實作

package com.example.redpacket;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public final class RedPacketAlgorithm {
    private RedPacketAlgorithm() {}

    /**
     * 核心演算法:二倍均值法 (使用 BigDecimal 避免浮點誤差)
     * @param totalAmount 總金額 (元,保留兩位小數)
     * @param count 紅包個數
     * @return 每個紅包金額列表 (元,BigDecimal)
     */
    public static List divide(BigDecimal totalAmount, int count) {
        // 參數驗證
        if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("總金額必須大於 0");
        }
        if (count <= 0) {
            throw new IllegalArgumentException("紅包個數必須大於 0");
        }
        // 最小金額限制:0.01 元
        BigDecimal minAmount = new BigDecimal("0.01");
        if (totalAmount.compareTo(minAmount.multiply(BigDecimal.valueOf(count))) < 0) {
            throw new IllegalArgumentException("總金額不足以分配 " + count + " 個最小紅包 (0.01元)");
        }

        List packets = new ArrayList<>(count);
        BigDecimal remainingAmount = totalAmount;
        int remainingCount = count;
        Random random = ThreadLocalRandom.current(); // 執行緒安全、高效能

        for (int i = 0; i < count; i++) {
            remainingCount = count - i;
            
            // 最後一包:直接分配剩餘
            if (remainingCount == 1) {
                packets.add(remainingAmount.stripTrailingZeros());
                break;
            }

            // 計算當前平均值
            BigDecimal avg = remainingAmount.divide(BigDecimal.valueOf(remainingCount), 10, RoundingMode.HALF_UP);
            // 上限 = 2 * 平均值
            BigDecimal maxAmount = avg.multiply(BigDecimal.valueOf(2));
            
            // 隨機生成 [0.01, maxAmount] 區間金額
            // 為了避免生成 0,最小設為 0.01
            BigDecimal min = minAmount;
            // 確保上限不超過剩餘金額 - (剩餘包數-1)*0.01 (保留後續最小額度)
            BigDecimal maxPossible = remainingAmount.subtract(minAmount.multiply(BigDecimal.valueOf(remainingCount - 1)));
            if (maxAmount.compareTo(maxPossible) > 0) {
                maxAmount = maxPossible;
            }
            
            // 生成隨機金額:min + random * (max - min)
            BigDecimal range = maxAmount.subtract(min);
            // 使用 scale=2 的隨機數 (分為單位)
            int minCents = min.movePointRight(2).intValueExact();
            int maxCents = maxAmount.movePointRight(2).intValueExact();
            
            if (minCents > maxCents) { // 邊界保護
                packets.add(min);
                remainingAmount = remainingAmount.subtract(min);
                continue;
            }
            
            int randomCents = random.nextInt(maxCents - minCents + 1) + minCents;
            BigDecimal packet = new BigDecimal(randomCents).movePointLeft(2);
            
            packets.add(packet.stripTrailingZeros());
            remainingAmount = remainingAmount.subtract(packet);
        }
        
        return packets;
    }

    /** 便利方法:double 版本 (內部轉 BigDecimal) */
    public static List divide(double totalAmount, int count) {
        return divide(BigDecimal.valueOf(totalAmount), count)
                .stream()
                .map(BigDecimal::doubleValue)
                .toList();
    }

    /** 驗證總和精確等於原金額 */
    public static boolean validateSum(List packets, BigDecimal expected) {
        BigDecimal sum = packets.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        return sum.stripTrailingZeros().compareTo(expected.stripTrailingZeros()) == 0;
    }
}

三、單元測試與統計驗證

import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.Assert.*;

public class RedPacketAlgorithmTest {

    @Test
    public void testBasicDivision() {
        List packets = RedPacketAlgorithm.divide(new BigDecimal("100.00"), 10);
        assertEquals(10, packets.size());
        assertTrue(RedPacketAlgorithm.validateSum(packets, new BigDecimal("100.00")));
        for (BigDecimal p : packets) {
            assertTrue(p.compareTo(new BigDecimal("0.01")) >= 0);
        }
    }

    @Test
    public void testEdgeCases() {
        // 最小金額剛好
        List packets = RedPacketAlgorithm.divide(new BigDecimal("0.10"), 10);
        assertEquals(10, packets.size());
        for (BigDecimal p : packets) {
            assertEquals(new BigDecimal("0.01"), p);
        }
        
        // 單個紅包
        packets = RedPacketAlgorithm.divide(new BigDecimal("50.00"), 1);
        assertEquals(1, packets.size());
        assertEquals(new BigDecimal("50.00"), packets.get(0));
    }

    @Test
    public void testStatisticalFairness() {
        // 大樣本驗證期望值接近均值
        int trials = 10000;
        int count = 5;
        BigDecimal total = new BigDecimal("100.00");
        BigDecimal expectedAvg = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP); // 20.00
        
        double[] sums = new double[count];
        for (int t = 0; t < trials; t++) {
            List packets = RedPacketAlgorithm.divide(total, count);
            for (int i = 0; i < count; i++) {
                sums[i] += packets.get(i).doubleValue();
            }
        }
        
        for (int i = 0; i < count; i++) {
            double actualAvg = sums[i] / trials;
            // 允許 5% 誤差範圍
            assertEquals(expectedAvg.doubleValue(), actualAvg, expectedAvg.doubleValue() * 0.05);
        }
    }
}

四、關鍵細節與避坑指南

問題原因解決方案
浮點數誤差導致總和不等double 二進位無法精確表示 0.01**全程使用 BigDecimal**,最後驗證總和
生成 0 元紅包random.nextDouble() 可能極小設定最小值 0.01 (分為單位整數隨機)
最後幾包金額異常大/小未保留後續最小額度上限取 min(2*avg, 剩餘 - (剩餘包數-1)*0.01)
高併發下 Random 競爭java.util.Random 使用 CAS 更新種子使用 **ThreadLocalRandom** (Java 7+)
金額精度超過兩位小數除法產生無限循環小數BigDecimal 設定 scale=2, RoundingMode.HALF_UP

五、擴展優化方案

1. 指數分佈 / 對數正態分佈 (更接近真實手氣分佈)

二倍均值法產生均勻分佈,實際微信紅包傾斜度更高 (少數大額、多數小額)。可引入 LogNormal 分佈:

// 簡化版:生成對數正態隨機數
double logNormal = Math.exp(ThreadLocalRandom.current().nextGaussian() * sigma + mu);
// 再歸一化到總金額

2. 預生成 + 打亂 (極高併發場景)

  • 後台定時批次預生成大量紅包組合,存入 Redis 列表。
  • 用戶搶紅包時 LPOP 直接取出,無運算延遲。
  • 定期補充池子,確保可用性。

3. 分佈式一致性 (多機房)

  • Redis Lua 腳本原子扣減庫存 + 分配金額。
  • 或使用分佈式鎖 (Redisson) 保護關鍵區段。

六、完整主程式測試入口

public class RedPacketDemo {
    public static void main(String[] args) {
        BigDecimal total = new BigDecimal("200.00");
        int count = 20;
        
        System.out.println("=== 微信紅包模擬: " + total + " 元, " + count + " 個 ===");
        List packets = RedPacketAlgorithm.divide(total, count);
        
        BigDecimal sum = BigDecimal.ZERO;
        for (int i = 0; i < packets.size(); i++) {
            BigDecimal p = packets.get(i);
            sum = sum.add(p);
            System.out.printf("第 %2d 包: %6.2f 元 (剩餘: %.2f)%n", 
                i+1, p, total.subtract(sum));
        }
        System.out.println("總計驗證: " + (sum.stripTrailingZeros().compareTo(total.stripTrailingZeros()) == 0 ? "✅ 通過" : "❌ 失敗"));
        
        // 統計
        BigDecimal max = packets.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
        BigDecimal min = packets.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
        BigDecimal avg = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP);
        System.out.printf("統計: 最大=%.2f, 最小=%.2f, 平均=%.2f, 理論平均=%.2f%n", max, min, 
            sum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP), avg);
    }
}

結語

二倍均值法以簡潔的數學期望保證公平性,配合動態上限控制極端值,是紅包演算法的經典實作。**生產環境必須使用 BigDecimal 處理金額、ThreadLocalRandom 保證併發效能、完善邊界條件防護**。理解其統計特性,可進一步擴展為更貼近真實分佈的進階演算法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved