使用 Redis 实现生成分布式全局唯一ID(使用SpringBoot环境实现)

目录

    • 一、前言
    • 二、如何通过Redis设计一个分布式全局唯一ID生成工具
      • 2.1、使用 Redis 计数器实现
      • 2.2、使用 Redis Hash结构实现
    • 三、通过代码实现分布式全局唯一ID工具
      • 3.1、编写获取工具
      • 3.2、测试获取工具
    • 四、总结

一、前言

       在很多项目中生成类似订单编号、用户编号等有唯一性数据时还用的UUID工具,或者自己根据时间戳+随机字符串等组合来生成,在并发小的时候很少出问题,当并发上来时就很可能出现重复编号的问题了,单体项目和分布式项目都是如此,要想解决这个问题也有很多种方法,可以自己写一个唯一ID生成规则,也可以通过数据库来实现全局ID生成这个和使用Redis实现其实类似,还可以使用比较成熟的雪花算法工具实现,每种方法都有各自的优缺点这里不展开说明,这里详细说明如何使用Redis实现生成分布式全局唯一ID。
       还有一个问题为什么不能直接使用数据库的自增ID,而是需要单独生成一个分布式全局唯一ID,类似订单IDON202311090001,在数据库中有自增ID,对于当前业务来说就是唯一的为什么不能用,还要去生成一个独立的订单ID,对于这个问题要从几个方面分析:
       1、数据库自增ID是有序增长的很容易就被人猜到,比如我现在下一单看到的订单ID为999那么就知道你的系统里最多只有999单,还有如果接口设计不合理,比如取消订单接口只校验了用户是否登录没有校验订单是否属于该用户,接收一个订单ID就能将订单取消,那么这样很容易就被人抓住漏洞,类似的情况有很多,也很多人写接口是不会注意这个问题。
       2、这种自增ID没有意义,而且不同业务的自增ID是重合的,对于信息区分度很低,而且考虑到多业务交互和用户端展示也都是不合适的,想想看要是你在某宝下单,订单ID是999,或者在对接别人订单系统时,给你的订单ID是999是不是很奇怪。
       3、分库分表时自增ID会重复

需要集成文章可以查看:
SpringBoot集成Lettuce客户端操作Redis:https://blog.csdn.net/weixin_44606481/article/details/133907103

二、如何通过Redis设计一个分布式全局唯一ID生成工具

       用户下单调用下单逻辑,先进行业务逻辑处理,然后携带订单ID标识通过分布式全局唯一ID工具获取一个唯一的订单ID,这个订单ID标识就是用于区分业务的,获取到订单ID后将数据组装入库,分布式全局唯一ID工具可以做成一个内嵌的utils,也可以封装成一个独立的jar,还可以做成一个分布式全局唯一ID生成服务供其它业务服务调用。

在这里插入图片描述

2.1、使用 Redis 计数器实现

       Redis的String结构提供了计数器自增功能,类似Java中的原子类,还要优于Java的原子类,因为Redis是单线程执行的缓存读写本身就是线程安全的,也不用进行原子类的乐观锁操作,每一次获取分布式全局唯一ID时就将自增序列加1。

# 给key为GENERATEID:NO的value自增1,如果这key不存在则会添加到Redis中并且设置value为1
## GENERATEID:key前缀
## NO:订单ID标识
127.0.0.1:6379> incr GENERATEID:NO
(integer) 1

2.2、使用 Redis Hash结构实现

       Redis Hash结构中的每一个field也可以进行自增操作,可以用一个Hash结构存储所有的标识信息和自增序列,方便管理,比较适合并发不高的小项目所有服务都是用的一个Redis,如果并发较高就不合适了,毕竟Redis操作普通String结构肯定比操作Hash结构快。

# 给key为GENERATEID,field为no的value自增1,如果这key不存在则会添加到Redis中并且设置value为1
## GENERATEID:分布式全局唯一ID Hash key
## NO:Hash结构中的field
127.0.0.1:6379> hincrby GENERATEID NO 1
(integer) 1

三、通过代码实现分布式全局唯一ID工具

       这里使用Redis 计数器实现,自增序列以天为单位存储,在实际业务中,比如生成订单编号组成规则都类似NO1699631999000-1(业务标识key+当前时间戳+自增序列),这个规则可以自己定义,保证最终生成的订单编号不重复即可,不建议直接一个自增序列干到底,订单编号这类型的数据都是有长度限制的,或者是要求生成20字符的订单编号,如果增长的过长反而不好处理。

3.1、编写获取工具

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.concurrent.TimeUnit;@Component
public class RedisGenerateIDUtils {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// key前缀private String PREFIX = "GENERATEID:";/*** 获取全局唯一ID* @param key 业务标识key*/public String generateId(String key) {// 获取对应业务自增序列Long incr = getIncr(key);// 组装最后的结果,这里可以根据需要自己定义,这里是按照业务标识key+当前时间戳+自增序列进行组装String resultID = key + System.currentTimeMillis() + "-" + incr;return resultID;}/*** 获取对应业务自增序列*/private Long getIncr(String key) {String cacheKey = getCacheKey(key);Long increment = 0L;// 判断Redis中是否存在这个自增序列,如果不存在添加一个序列并且设置一个过期时间if (!redisTemplate.hasKey(cacheKey)) {// 这里存在线程安全问题,需要加分布式锁,这里做简单实现String lockKey = cacheKey + "_LOCK";// 设置分布式锁boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS);if (!lock) {// 如果没有拿到锁进行自旋return getIncr(key);}increment = redisTemplate.opsForValue().increment(cacheKey);// 我这里设置24小时,可以根据实际情况设置当前时间到当天结束时间的插值redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);// 释放锁redisTemplate.delete(lockKey);} else {increment = redisTemplate.opsForValue().increment(cacheKey);}return increment;}/*** 组装缓存key*/private String getCacheKey(String key) {return PREFIX + key + ":" + getYYYYMMDD();}/*** 获取当前YYYYMMDD格式年月日*/private String getYYYYMMDD() {LocalDate currentDate = LocalDate.now();int year = currentDate.getYear();int month = currentDate.getMonthValue();int day = currentDate.getDayOfMonth();return "" + year + month + day;}
}

3.2、测试获取工具

import com.redisscene.utils.RedisGenerateIDUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.*;@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisSceneApplication.class)
public class RedisGenerateIDTest {@Autowiredprivate RedisGenerateIDUtils redisGenerateIDUtils;@Testpublic void t1() throws InterruptedException {// 定义一个线程池 设置核心线程数和最大线程数都为100,队列根据需要设置ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000));CountDownLatch countDownLatch = new CountDownLatch(10000);long beginTime = System.currentTimeMillis();// 获取10000个全局唯一ID 看看是否有重复CopyOnWriteArraySet<String> ids = new CopyOnWriteArraySet<>();for (int i = 0; i < 10000; i++) {executor.execute(() -> {// 获取全局唯一IDlong beginTime02 = System.currentTimeMillis();String orderNo = redisGenerateIDUtils.generateId("NO");System.out.println("获取单个ID耗时 time=" + (System.currentTimeMillis() - beginTime02));if (ids.contains(orderNo)) {System.out.println("重复ID=" + orderNo);} else {ids.add(orderNo);}countDownLatch.countDown();});}countDownLatch.await();// 打印获取到的全局唯一ID集合数量System.out.println("获取到全局唯一ID count=" + ids.size());System.out.println("耗时毫秒 time=" + (System.currentTimeMillis() - beginTime));}
}

在这里插入图片描述

四、总结

       通过测试可以看到100并发生成全局唯一ID是没问题的,而且获取单个ID耗时在10-20毫秒左右,一般的业务已经完全够用,这个耗时也要看Redis性能和项目配置决定的,如果对于这种唯一ID生成并发量非常高的业务,可以提前生成一个唯一ID池存储在本地内存中,业务要获取唯一ID先去池中获取,如果获取不到再去Redis获取,自增序列一次性增加多个,然后将这个区间的值存储在本地缓存即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/137002.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# 时区时间与本地时间的相互转换

//时区时间与本地时间的相互转换&#xff0c;方法一&#xff1a; var localTime TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow); var utcTime TimeZone.CurrentTimeZone.ToUniversalTime(DateTime.Now); //时区时间与本地时间的相互转换&#xff0c;方法二&#…

打印流详解

概述 作用&#xff1a;打印流可以实现方便、高效的打印数据到文件中去。 高效体现在用到了缓冲流&#xff1a; public PrintStream(OutputStream out, boolean autoFlush, Charset charset) {super(out);this.autoFlush autoFlush;this.charOut new OutputStreamWriter(thi…

Linux文件管理知识:文本处理

上篇文章详细介绍了Linux系统中查找文件的工具或者命令程序的相关操作内容介绍。那么&#xff0c;今天呢&#xff0c;这篇文章围绕Linux系统中文本处理来阐述。 众所周知&#xff0c;所有Linux操作系统都离不开一个核心原则&#xff0c;那就是它是由很多种文件组成的&#xff0…

汇编-EQU伪指令(数值替换)

EQU伪指令将一个符号名称与一个整数表达式或一个任意文本相关联&#xff0c; 它有3种格式 在第一种格式中&#xff0c; expression必须是一个有效的整数表达式。在第二种格式中&#xff0c; symbol是一个已存在的符号名称&#xff0c; 已经用或EQU定义过。在第三种格式中&…

温故知新:探究Android UI 绘制刷新流程

一、说明&#xff1a; 基于之前的了解知道ui的绘制最终会走到Android的ViewRootImpl中scheduleTraversals进行发送接收vsync信号绘制&#xff0c;在ViewRootImpl中还会进行主线程检测&#xff0c;也就是我们所谓子线程更新ui会抛出异常。 像我们常用的刷新ui&#xff0c;inval…

牛客竞赛网(爱吃素)

题目描述 牛妹是一个爱吃素的小女孩&#xff0c;所以很多素数都害怕被她吃掉。 一天&#xff0c;两个数字aaa和bbb为了防止被吃掉&#xff0c;决定和彼此相乘在一起&#xff0c;这样被吃掉的风险就会大大降低&#xff0c;但仍有一定的可能被吃掉&#xff0c;请你判断他们相乘后…

智能指针的理解

题目 为什么要定义智能指针&#xff1f;智能指针的原理RALL 智能指针的使用std::auto_ptrstd::unique_ptrstd::shared_ptr 为什么要定义智能指针&#xff1f; 之前定义指针申请内存空间使用后要进行delete进行资源释放&#xff0c;但是如果在资源释放之前就抛出异常&#xff0…

【Git】git的安装与使用教程

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Git》。&#x1f3af;&#x1f3af; &#x1f449…

麒麟KYLINIOS软件仓库搭建03-软件仓库添加新版本的软件包

原文链接&#xff1a;麒麟KYLINIOS软件仓库搭建03-软件仓库添加新版本的软件包 hello&#xff0c;大家好啊&#xff0c;今天给大家带来麒麟桌面操作系统软件仓库搭建的文章03-软件仓库添加新版本的软件包&#xff0c;本篇文章主要给大家介绍了如何在麒麟桌面操作系统2203-x86版…

蓝桥杯每日一题2023.11.9

包子凑数 - 蓝桥云课 (lanqiao.cn) 题目描述 题目分析 对于此题是一个简单DP的翻版问题&#xff0c;若能凑出当前的包子数&#xff0c;则凑出之前一定为dp[i - a[j]]&#xff0c;若表示出的dp[i]不是0则说明是一定存在数可以被凑出的&#xff0c;由题意&#xff1a;若凑不出的…

ubuntu 设置最大带宽

背景 近日做实验&#xff0c;需要限制一些机子的带宽以达到模拟的效果。在网上搜索了一阵子&#xff0c;结合自己实操的经验&#xff0c;潦草写下这篇文章&#xff0c;供自己与有需要的人参考。 环境&#xff1a; Ubuntu 22.04.1 LTS 安装 wondershaper 和 speedtest-cli w…

非关系数据库

非关系数据库nosql 用来解决特定问题的数据库 特点&#xff1a; 1.没有关系模式schema-free/non-relational&#xff0c;与关系数据库不同 2.快速处理rapid process&#xff0c;数据放在内存中处理 3.distributed process分布式 4.big data 5.easy program 6.open-sour…

金融工作怎么做?低代码如何助力金融行业

10月30日至31日&#xff0c;中央金融工作会议在北京举行。金融是国民经济的“血脉”&#xff0c;是国家核心竞争力的重要组成部分。会议指出&#xff0c;党的十八大以来&#xff0c;在党中央集中统一领导下&#xff0c;金融系统有力支撑经济社会发展大局&#xff0c;坚决打好防…

uniapp获取设备mac地址

const net plus.android.importClass(java.net.NetworkInterface);const wlan0 net.getByName(wlan0);const macByte wlan0.getHardwareAddress();let macStr ;macByte.forEach(item > {// .toString(16)数字以十六进制值显示let temp ;if (item < 0) temp (256 i…

MarkDown基础及表格、KaTeX公式、矩阵、流程图、UML图、甘特图语法

概述 最多可设置6级标题 技巧 列表 有序列表 MD语法&#xff1a; 1. 你好 2. 我也好呈现效果&#xff1a; 你好我也好 无序列表 MD语法&#xff1a; - a - b * aa * bbaaabbb效果&#xff1a; ab aabb aaabbb 结论&#xff0c;支持三种方式&#xff1a;-、*、 T…

Vue集成海康websdk实现摄像头预览

选择以及下载相应的websdk&#xff1a; 从海康开放平台下载相应的sdk&#xff0c;web3.0不支持高版本浏览器&#xff0c;web3.2需要摄像头支持摄像头取流&#xff0c;web3.3支持高版本浏览器 我这选择的是3.3的。可以先测试下开发包是否可以成功访问&#xff0c;修改用ip、户名…

android开发布局知识

插件开发的视频笔记&#xff1a;

Aop天花板

拒绝废话,拒绝冗余文字,直接上代码和案例 1 导入依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency> 2 通知类配置解释(各个配置含义在代码中解释): packag…

算法进阶指南图论 通信线路

通信线路 思路&#xff1a;我们考虑需要升级的那条电缆的花费&#xff0c;若其花费为 w &#xff0c;那么从 1 到 n 的路径上&#xff0c;至多存在 k 条路径的价值大于 w &#xff0c;这具有一定的单调性&#xff0c;当花费 w 越大&#xff0c;我们路径上价值大于 w 的花费会越…

25期代码随想录算法训练营第十三天 | 栈与队列 part 2

目录 239. 滑动窗口最大值347.前 K 个高频元素方法一方法二 239. 滑动窗口最大值 链接 窗口 — 维持一个单调递增队列 为什么要使用队列&#xff1f; 在窗口移动的时候&#xff0c;方便把不属于窗口的最大值剔除。&#xff08;当窗口移动之后&#xff09; class Solution:…