「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」
一、前言
抽奖活动中有个需求:
- 用户能自主发起抽奖活动
- 其他用户能够通过活动码来加入抽奖活动
活动码取值范围为:0 ~ 9999 9999
即二进制中为 26 位。
那么这个活动码如何生成呢?
先了解下,当前几种 Id 生成方式:
- 方案一:利用数据库的自增
ID字段
CREATE TABLE user (
uid BIGINT PRIMARY KEY AUTO_INCREMENT,
uname VARCHAR(500) NOT NULL
) AUTO_INCREMENT = 1;
- 方案二:
GUID折半 + 冲突处理
理论上会存在冲突,当冲突时: 再取一次
GUID即可。
// 伪代码
// 1. 生成一个 128位的 guid
uint128_t* p_guid = new GUID();
uint64_t uid = *(uint64_t*) p_guid;
- 方案三:当前微秒数 + 冲突处理
// 伪代码
// 取当前微秒数
uint64_t uid = GetTickCount();
// 理论上会存在冲突,当再冲突时,再次获取当前微秒数
- 方案四:用户名哈希折半 + 冲突处理
// 伪代码
char[16] sign = md5(uname);
uint64_uid = *(unit64_t*) sign;
// 截取:可以取前半段、后半段,或者异或处理后
// 理论上会存在冲突。
- 方案五:分布式
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算法)
这里设计思路是:
- 活动码不重复
- 难以猜测
要做到不重复,可以考虑一下 "时间+序号" 的因数,能保证不重复; 而要难以猜测,则可以做一点混淆。
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) |
------------------
然后对上面的数据,进行混淆。
混淆算法,基本上是采用一个对调算法。
小结:
-
唯一性很容易证明(注意:ID 在 4年内唯一,非永久唯一)
-
为什么难以猜测? 这种方法可以保证:序号 + 盐值,是一个比较随机的现象,那么还是很难猜测到的数。
这个序列号怎么得到? 可以通过数据库的自增
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