Redis实战9-全局唯一ID
发布优惠券的时候,每个店铺都可以发布优惠券,当用户抢购的时候,优惠券表中的id如果使用数据库的自增长ID会存在以下问题:
1:id的规律太明显,容易被刷
2:当数据量很大的时候,会受到单表数据的限制
缺点场景分析:
id规律场景:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。
单表限制:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。
全局ID生成器
全局ID生成器,是一种在分布式系统下用来生成全局唯一的ID工具,一般需要瞒住下列特性:
唯一性、高可用、递增性、安全性、高性能
全局唯一ID生成策略:
UUID、Redis自增长、雪花算法、数据库自增
Redis自增ID策略:
1:每天一个key,方便统计订单量;
2:ID都在是时间戳+计数器
实战:基于Redis拼接其他信息来实现全局唯一ID
全局唯一ID使用long类型的,其中时间戳是基于某一个时间点开始的。比如我们从2022.11.26 23:00:00开始,可以使用69年。
思考1:获取当前时间的秒:
思考2:时间戳,怎么计算的?
使用当前时间的秒-初始时间的秒
思考3:序列号怎么设置?
使用Redis的setnx命令,最好加上当前年月日
思考4:怎么拼接?
因为我们需要返回的是long类型的,如果使用string拼接后,还要转换。还要知道,使用string拼接,当并发量很大的时候,也会有性能问题。那么我们应该怎么处理的?
注意:我们再来看看全局唯一ID的格式。如上图,我们可以看出,共64位,其中符号位是1个,时间戳是31位。序列号是32位,发现什么了吗?如果我们把时间戳向左移动32位(因为序列号是32位。向左移动位置,空出给序列号使用),是不是就是符号位+时间戳的了?
1:我们也知道计算机中左移动最快是x<<位数。
2:我们还需要知道,在计算机中 | 或计算:按位或运算“|”
根据上面,我们可以知道位运算序号后,就是序列号的值。序列号是多少,就是多少。
所以,我们可以知道拼接代码如下:timeStamp << 32 |no;
本文由凯哥Java(公众号:kaigejava),个人博客:www.kaigejava.com 发布于博客园。
小福利
凯哥自己开发的,领取外卖、打车、咖啡、买菜、各大电商的优惠券的公¥众¥号。如下图:
最终代码:
1 | import org.springframework.data.redis.core.StringRedisTemplate;<br> import org.springframework.stereotype.Component;<br> <br> import java.time.LocalDateTime;<br> import java.time.ZoneOffset;<br> import java.time.format.DateTimeFormatter;<br> <br> /**<br> * @author 凯哥Java<br> * @description 基于Redis实现62位的全局唯一ID<br> * @company<br> * */ <br>@Component<br> public class RedisIdWorker {<br> <br> private static final long BEGIN_TIMESTAMP = 1669503600L;<br> <br> <br> private final StringRedisTemplate stringRedisTemplate;<br> <br> /**<br> * 序列号的位数<br> */ <br> private static final int COUNT_BITS = 32 ;<br> <br> public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {<br> this .stringRedisTemplate = stringRedisTemplate;<br> }<br> <br> /**<br> * 获取id<br> * @param kyePrefix<br> * @return<br> */ <br> public long nextId( String kyePrefix){<br> //1:生成时间戳 = 当前时间戳-开始时间戳<br> LocalDateTime now = LocalDateTime.now();<br> long nowSecond = now.toEpochSecond(ZoneOffset.UTC);<br> long timeStamp = nowSecond - BEGIN_TIMESTAMP;<br> <br> //2:生成序列号.使用sexNx.其中加上当前年月日<br> //获取当前时间的你那月日<br> String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));<br> //开始32位序列号<br> long no = stringRedisTemplate.opsForValue().increment("icr:"+kyePrefix+":"+date);<br> //3:拼接返回<br> return timeStamp << COUNT_BITS |no;<br> <br> }<br> /**<br> * 获取到指定时间的毫秒<br> * @param args<br> */<br> public static void main(String[] args) {<br> LocalDateTime time = LocalDateTime.of(2022,11,26,23,00,00);<br> long second = time.toEpochSecond(ZoneOffset.UTC);<br> System.out.println(second);<br> <br> }<br>}<br> <br>测试:使用多线程及countdownlatch<br>private ExecutorService executorService = Executors.newFixedThreadPool(500);<br> <br> @Resource<br> private RedisIdWorker redisIdWorker;<br> <br> @Test<br> public void RedisIdWorkerTest() throws InterruptedException {<br> CountDownLatch latch = new CountDownLatch(300);<br> Runnable task = ()->{<br> for(int i = 0;i<100;i++){<br> long id = redisIdWorker.nextId("myorder");<br> System.out.println(id);<br> }<br> latch.countDown();<br> };<br> <br> long begin = System.currentTimeMillis();<br> for(int i = 0;i< 300;i++){<br> executorService.submit(task);<br> }<br> latch.await();<br> long endTime = System.currentTimeMillis();<br> System.out.println("耗时:"+(endTime-begin));<br> } |
本文来自博客园,作者:kaizi1992,转载请注明原文链接:https://www.cnblogs.com/kaigejava/p/17071825.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体