优雅实现短id生成,看这篇就够了!
业务背景:
在互联网项目中常常需要用户短id,所谓用户短id即是提供用户使用的id,比方说用户与用户之间加好友所用的id,针对这种场景把数据库的user_id提供给用户貌似不太合理,20位的userId,即影响页面显示时的美感,又不方便用户记忆,所以需要一个”短id“来作为userId的副本存在。
那怎么获取这个短id,怎么保证这个id的唯一性呢?短id作为用户属性之一必然会存在数据库中但为了确保id的唯一性貌似少不了循环比较,那有什么办法可以一个操作就可以实现"唯一"和生成id呢,看完本文保你通透。
一、核心lua脚本逻辑
生成用户唯一短id的核心逻辑如下所示,重点关注主体逻辑,关于入参的特殊处理本文末尾会讲到(针对阿里云Redis集群的特殊性处理)
local random_start = KEYS[2]
random_start = string.gsub(random_start, "{user_default_id}", "")
//首先将标志位log的初始值设置为0
local log = '0'
//调用 Redis 的 bitpos 命令,查找键 KEYS[1] 中值为 0 的第一个位的位置
//第三个参数 random_start 是起始搜索的位置。如果找到了,将位置赋给 pos;如果没有找到(即返回 -1)
local pos = redis.call('bitpos', KEYS[1], 0, random_start)
//如果没找到,代表从random_start处开始找寻并没有找到
if pos == -1 then
log = '-1'
//在整个位图中再次调用 bitpos 查找值为 “0” 的位的位置,并将结果赋给 pos
pos = redis.call('bitpos', KEYS[1], 0)
end
//如果依旧没有找到,代表在整个位图中都没有找到,将log赋值为-2以便程序中做判断
if pos == -1 then
log = '-2'
else
//如果找到了则将对应的位置的值设置为“1”
redis.call('setbit', KEYS[1], pos, 1)
end
return log..'|'..pos;
二、代码逻辑
2.1 初始化key的bit位
由于上面lua脚本逻辑的目的就是找寻值为“0“的位,那么首先我们需要初始化用于生成短id的RedisKey的所有位,初始化Key代码如下:
redisTemplate.opsForValue().setBit("{your_redisKey}", 10000000, false);
这里要注意一下,执行setBit命令时候,假如原本没有这个Key那会新建这个Key并且把0~10000000的所有位都置成指定的值(0/1)
2.2 执行lua脚本
这里需要提醒一下,为什么new SecureRandom().nextInt()的入参是1250000呢?因为在上面初始化bit位的时候我们初始化了0~10000000,而我们通过lua脚本去查询的位其实是字节的bit位,而1字节 = 8bit,也就是说1250000(字节)刚好占10000000个bit位
public Long getUserRandomId(long userId) {
try {
int random_start = new SecureRandom().nextInt(1250000);
List<String> keys = Arrays.asList("{your_redisKey}", String.format("{your_redisKey}", random_start));
//执行lua脚本
Object execute = authRedisTemplate.execute(redisLuaScriptLoader.getByScriptName("account.lua"), keys);
//由于我们的脚本返回的参数是log和pos拼接的,所以这里只要获取pos就可以了
String s = execute.toString().split("\\|")[1];
//由于lua脚本获取的是位,是一个七位的整数,所以这里补一位补成8位的短id
int i1 = Integer.parseInt(s) + (random_key +1)* 10000000;
return (long) i1;
} catch (Exception e) {
log.error("生成用户唯一短id错误", e);
return -1L;
}
}
三、踩坑
现在回过头来看lua脚本的入参,可以看到random_start是通过Key的传递传进lua脚本的,然后在脚本中通过字符替换的方式解出random_start ,那为什么不通过args[]传参方式把random_start传入呢,注意此处有大坑,因为阿里云Redis不支持args[]传参,细心的同学可能已经发现一个问题,这个Key为什么带的中括号{},因为在Redis集群环境下,是通过计算{}里面的字符得到的槽位,如果传入的两个Key的槽位不同会导致服务启动报错,当然单节点的Redis无需考虑此问题
lua调用如下:
List<String> keys = Arrays.asList("{your_redisKey}",
String.format("{your_redisKey}", random_start));
//执行lua脚本
Object execute = authRedisTemplate.execute(redisLuaScriptLoader.getByScriptName("account.lua"), keys);
lua解参如下:
local random_start = KEYS[2]
random_start = string.gsub(random_start, "{user_default_id}", "")
OK,本次分享到此就结束啦,如果大家有更优雅的方案也欢迎大家来评论区讨论
菜鸟日记持续更新... ...