【小设计】抽奖活动分布式 Id生成

302 阅读3分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

一、前言

抽奖活动中有个需求:

  1. 用户能自主发起抽奖活动
  2. 其他用户能够通过活动码来加入抽奖活动

活动码取值范围为:0 ~ 9999 9999

即二进制中为 26 位。

那么这个活动码如何生成呢?

先了解下,当前几种 Id 生成方式:

  1. 方案一:利用数据库的自增 ID 字段
CREATE TABLE user (
  uid BIGINT PRIMARY KEY AUTO_INCREMENT,
  uname VARCHAR(500) NOT NULL
) AUTO_INCREMENT = 1;
  1. 方案二:GUID 折半 + 冲突处理

理论上会存在冲突,当冲突时: 再取一次 GUID 即可。

// 伪代码
// 1. 生成一个 128位的 guid
uint128_t* p_guid = new GUID();
uint64_t uid = *(uint64_t*) p_guid;
  1. 方案三:当前微秒数 + 冲突处理
// 伪代码
// 取当前微秒数
uint64_t uid = GetTickCount();

// 理论上会存在冲突,当再冲突时,再次获取当前微秒数
  1. 方案四:用户名哈希折半 + 冲突处理
// 伪代码
char[16] sign = md5(uname);
uint64_uid = *(unit64_t*) sign;

// 截取:可以取前半段、后半段,或者异或处理后

// 理论上会存在冲突。
  1. 方案五:分布式 ID 生产算法(snowflake 算法原理)

会将以下信息拼接成 64bit。

1. 符号位(1位)
2. 毫秒数(41位)
3. 机房分组(节点 id 一部分,5位)
4. 机器分组(节点 id 一部分,5位)
5. 同一毫秒内计数(12位)

每秒可生成多少 Id

1000毫秒 × 32个机房 × 32个节点 × 4096(12bit) = 40亿个不重复的 Id



二、实战

参考方案五:分布式 ID 算法(sonwflake 算法)

这里设计思路是:

  1. 活动码不重复
  2. 难以猜测

要做到不重复,可以考虑一下 "时间+序号" 的因数,能保证不重复; 而要难以猜测,则可以做一点混淆。

26位活动码设计划分如下:

0~8 bit 表示:
-------------------------------------------------
| 8 | 7 | 6 | 5 | 4 | 3 | 2 |    1    |    0    |
|-----------------------------------------------|
|(日期-1)/4 |	月份      | 年份-2位循环(4年)  |
-------------------------------------------------


9~21 bit 表示:
-----------------------------------------------------
| 21 20	19 18 17 16 15 14 13 12	11 10 9             |
|---------------------------------------------------|
| 序列号(8192个)                                    |
-----------------------------------------------------


22~25 bit 表示:
------------------
| 25 24	23 22    |
|--------------- |
| 盐值(随机的0,1)  |
------------------

然后对上面的数据,进行混淆。

混淆算法,基本上是采用一个对调算法。

小结:

  1. 唯一性很容易证明(注意:ID 在 4年内唯一,非永久唯一)

  2. 为什么难以猜测? 这种方法可以保证:序号 + 盐值,是一个比较随机的现象,那么还是很难猜测到的数。

    这个序列号怎么得到? 可以通过数据库的自增 ID,比如获取到自增 ID % 8192,就行了,可以先插入一条记录,得到了 ID 之后,再做 HASH

@Slf4j
public class ActivityCodeUtils {

    private static final int[] shuffle = {5, 16, 24, 12, 13, 7, 22, 14, 19, 23, 3, 
             10, 0, 25, 18, 4, 11, 20, 8, 15, 17, 9, 21, 1, 6, 2};

    private static final long[] pow = new long[shuffle.length];
    private static final int[] offset = new int[shuffle.length];

    static {
        for (int i = 0; i < shuffle.length; i++) {
            pow[i] = 1L << shuffle[i];
            offset[i] = Math.abs(shuffle[i] - i);
        }
    }

    public static String genActivityCode(long id) {

        long code = generateActivityCode(id);

        String activityCode = String.valueOf(code);

        return executeActivityCode(activityCode);
    }

    /**
     * 处理活动码
     
     * Tips: 互动码位数要求, 不足补0, 过长截取.
     *
     * @param activityCode 活动码
     * @return 处理后的活动码
     */
    private static String executeActivityCode(String activityCode) {

        if (StringUtils.isEmpty(activityCode)) {

            return "00000000";
        }

        int len = activityCode.length();

        if (len < Constants.ACTIVITY_CODE_LEN_MAX) {

            return String.format("%1$" + Constants.ACTIVITY_CODE_LEN_MAX + "s", 
                                 activityCode)
                    .replace(' ', '0');
        }

        return activityCode.substring(len - Constants.ACTIVITY_CODE_LEN_MAX, len);
    }

    private static long generateActivityCode(long id) {

        Calendar calendar = Calendar.getInstance();

        long year = calendar.get(Calendar.YEAR) % 4;
        long month = calendar.get(Calendar.MONTH) + 1;
        long day = (calendar.get(Calendar.DAY_OF_MONTH) - 1) / 4;

        Random random = new Random();
        long salt = random.nextInt(16);

        long value = id % 8192;

        long preShuffle = (salt << 22) | (value << 9) | (day << 6) | (month << 2) | year;

        return shuffle(preShuffle);
    }

    private static long shuffle(long pre) {
        long sum = 0;

        for (int i = 0; i < shuffle.length; i++) {
            if (shuffle[i] >= i) {
                sum |= (pre << offset[i]) & pow[i];
            } else {
                sum |= (pre >> offset[i]) & pow[i];
            }
        }

        return sum;
    }

    public static void main(String[] args) {

        for (int i = 1; i < 20; ++i) {

            System.out.println(genActivityCode(i));
        }
    }
}

测试,输出结果如下:

14749764
06361102
12652554
06362118
12653636
06362126
12653580
04263941
14749761
06361103
12652621
06362113
12653633
04265039
12653647
37818368
48304128
37818444
48304142