架构设计 | 分布式业务系统中,全局ID生成策略
本文源码:GitHub·点这里 || GitEE·点这里
一、全局ID简介
在实际的开发中,几乎所有的业务场景产生的数据,都需要一个唯一ID作为核心标识,用来流程化管理。比如常见的:
- 订单:order-id,查订单详情,物流状态等;
- 支付:pay-id,支付状态,基于ID事务管理;
如何生成唯一标识,在普通场景下,一般的方法就可以解决,例如:
import java.util.UUID; public class UuidUtil { public static String getUUid() { UUID uuid = UUID.randomUUID(); return String.valueOf(uuid).replace("-",""); } }
这个方法可以解决绝大部分唯一ID需求的场景业务,但是网上各种UUID重复场景的描述帖,说的好像该API不好用。
絮叨一句
:说一个真实使用的业务场景,大概是半年近3000万的数据流水,用的就是UUID的API,暂时未捕捉到ID重复的问题,仅供参考。
二、雪花算法
1、概念简介
Twitter公司开源的分布式ID生成算法策略,生成的ID遵循时间的顺序。
- 1为位标识,始终为0,不可用;
- 41位时间截,存储时间截的差值(当前时间截-开始时间截);
- 10位的机器标识,10位的长度最多支持部署1024个节点;
- 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序号;
SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高。
2、编码实现
工具类中很多可以自定义的,比如起始时间,机器ID配置等。
/** * 雪花算法ID生成 */ public class SnowIdWorkerUtil { // 开始时间截 (2020-01-02) private final long timeToCut = 1577894400000L; // 机器ID所占的位数 private final long workerIdBits = 2L; // 数据标识ID所占的位数 private final long dataCenterIdBits = 8L; // 支持的最大机器ID,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 支持的最大数据标识ID,结果是31 private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); // 序列在ID中占的位数 private final long sequenceBits = 12L; // 机器ID向左移12位 private final long workerIdShift = sequenceBits; // 数据标识ID向左移17位(12+5) private final long dataCenterIdShift = sequenceBits + workerIdBits; // 时间截向左移22位(5+5+12) private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; // 生成序列的掩码 private final long sequenceMask = -1L ^ (-1L << sequenceBits); // 工作机器ID(0~31) private long workerId; // 数据中心ID(0~31) private long dataCenterId; // 毫秒内序列(0~4095) private long sequence = 0L; // 上次生成ID的时间截 private long lastTimestamp = -1L; /** * 构造函数 * @param workerId 工作ID (0~31) * @param dataCenterId 数据中心ID (0~31) */ public SnowIdWorkerUtil (long workerId, long dataCenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException("workerId 不符合条件"); } if (dataCenterId > maxDataCenterId || dataCenterId < 0) { throw new IllegalArgumentException("dataCenterId 不符合条件"); } this.workerId = workerId; this.dataCenterId = dataCenterId; } public synchronized String nextIdVar(){ return String.valueOf(nextId()); } /** * 线程安全,获得下一个ID */ private synchronized long nextId() { long timestamp = timeGen(); // 如果当前时间小于上一次ID生成的时间戳,抛出异常 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "时间顺序异常,时间差(上次时间-现在)=%d", lastTimestamp - timestamp)); } // 如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒内序列溢出 if (sequence == 0) { //阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } else { // 时间戳改变,毫秒内序列重置 sequence = 0L; } // 上次生成ID的时间截 lastTimestamp = timestamp; // 移位并通过或运算拼到一起组成64位的ID return ((timestamp - timeToCut) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) | (workerId << workerIdShift) | sequence; } /** * 阻塞,获得新的时间戳 */ private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回当前时间节点 */ private long timeGen() { return System.currentTimeMillis(); } public static void main(String[] args) { // 参数在实际业务下需要配置管理 SnowIdWorkerUtil idWorker = new SnowIdWorkerUtil(1, 1); for (int i = 0; i < 100; i++) { String id = idWorker.nextIdVar(); System.out.println(id+" "+id.length()+"位"); } } }
三、自定义实现
还有一种常见的实现思路,基于数据库的自增主键ID,不过基于这个原理,却有各种不同的实现策略。
简单表结构:
CREATE TABLE `du_temp_id` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='主键ID临时表';
1、基于主键
这种模式的原理比较单调,向临时表写入一条记录,借助MySQL生成的唯一主键ID,然后拿出来稍微处理一下,作为各种业务场景的唯一ID使用。
@Service public class TempIdServiceImpl implements TempIdService { @Resource private TempIdMapper tempIdMapper ; @Override public List<String> getIdList() { List<String> idList = new ArrayList<>() ; TempIdEntity tempIdEntity = new TempIdEntity (); tempIdEntity.setCreateTime(new Date()); for (int i = 0 ; i < 10 ; i++){ tempIdMapper.insert(tempIdEntity); idList.add(UuidUtil.getNoId(8,Long.parseLong(tempIdEntity.getId().toString()))) ; } return idList ; } }
问题点:如果作为ID生成的临时表所在的MySQL服务宕掉,那可能会影响整个业务流程,造成雪崩效应。
2、高可用集群
单服务如果不能安稳的支撑业务需求,很自然集群模式就该上场了。提供多台MySQL服务[A,B,C],处理策略也不止一种:
- 库设置主键自增策略
例如A库[1,4,7],B库[2,5,8],C库[3,6,9],基于不同自增规则,生成统一的自增唯一标识。
- 生成ID做分库标识
这种先把ID生成,然后不同的数据库生成的ID给一个不同的标识,例如UIDA,UIDB,UIDC。
@Service public class TempIdServiceImpl implements TempIdService { @Resource private TempIdMapper tempIdMapper ; @Override public List<String> getRouteIdList() { List<String> idList = new ArrayList<>() ; TempIdEntity tempIdEntity = new TempIdEntity (); tempIdEntity.setCreateTime(new Date()); for (int i = 0 ; i < 2 ; i++){ tempIdMapper.insertA(tempIdEntity); idList.add(UuidUtil.getRouteId("UID-A",10, Long.parseLong(tempIdEntity.getId().toString()))) ; tempIdMapper.insertB(tempIdEntity); idList.add(UuidUtil.getRouteId("UID-B",10, Long.parseLong(tempIdEntity.getId().toString()))) ; tempIdMapper.insertC(tempIdEntity); idList.add(UuidUtil.getRouteId("UID-C",10, Long.parseLong(tempIdEntity.getId().toString()))) ; } return idList ; } }
结果样例:
UID-A00001,UID-B00001,UID-C00001
UID-A00002,UID-B00002,UID-C00002
3、ID样式优化
从数据获取的ID基本是一个自增的整数序列,可以提供一个格式美化工具方法。
public class UuidUtil { private static final String ZERO = "00000000000"; private static final String PREFIX = "UID"; public static String getNoId(int length,Long id){ String idVar = String.valueOf(id) ; if (idVar.length()>length){ return PREFIX+idVar ; } else { int gapLen = length-idVar.length()-PREFIX.length() ; return PREFIX+ZERO.substring(0,gapLen)+idVar ; } } public static String getRouteId(String route,Integer length,Long id){ String idVar = String.valueOf(id) ; if (idVar.length()>length){ return route+idVar ; } else { int gapLen = length-idVar.length()-route.length() ; return route+ZERO.substring(0,gapLen)+idVar ; } } }
基于不同的策略,把ID格式为统一的位数。
4、性能问题
如果在高并发的业务场景下,实时基于MySQL去生成唯一ID容易产生性能瓶颈,当然其他方法也可能产生这个问题。可以在系统空闲时间批量生成一批,放入缓存中,在使用的时候直接从缓存层取出即可。
四、源代码地址
GitHub·地址 https://github.com/cicadasmile/data-manage-parent GitEE·地址 https://gitee.com/cicadasmile/data-manage-parent
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
挑战全网最幽默的Vuex系列教程:第六讲 Vuex的管理员Module(实战篇)
写在前面 这一讲是 Vuex 基础篇的最后一讲,也是最为复杂的一讲。如果按照官方来的话,对于新手可能有点难以接受,所以想了下,决定干脆多花点时间,用一个简单的例子来讲解,顺便也复习一下之前的知识点。 首先还是得先了解下 Module 的背景。我们知道,Vuex 使用的是单一状态树,应用的所有状态会集中到一个对象中。如果项目比较大,那么相应的状态数据肯定就会更多,这样的话,store 对象就会变得相当的臃肿,非常难管理。 这就好比一家公司只有老板一个人来管理一样。如果小公司倒还好,公司要是稍微大一点,那就麻烦了。这个时候,老板就会成立各大部门,并给各大部门安排一个主管,把管理的任务分派下去,然后有什么事情需要处理的话,只需要跟这几个主管沟通,由主管再把任务分配下去就行了,这就大大提高了工作效率,也减轻了老板的负担。 那么同样的道理,Module 其实就承担了部门管理员的角色,而 store 就是老板。理解了这一层,那么后面就好办多了,接下来,咱们就一步一步动起手来开始实践。 一、准备工作 这里我们使用官方提供的 vue-cli 来建一个项目「vuex-test」。当然,先得安装 vue-...
- 下一篇
一不小心实现了RPC
前言 随着最近关注 cim 项目的人越发增多,导致提的问题以及 Bug 也在增加,在修复问题的过程中难免代码洁癖又上来了。 看着一两年前写的东西总是怀疑这真的是出自自己手里嘛?有些地方实在忍不住了便开始了漫漫重构之路。 前后对比 在开始之前先简单介绍一下 cim 这个项目,下面是它的架构图: 简单来说就是一个 IM 即时通讯系统,主要有以下部分组成: IM-server 自然就是服务端了,用于和客户端保持长连接。 IM-client 客户端,可以简单认为是类似于的 QQ 这样的客户端工具;当然功能肯定没那么丰富,只提供了一些简单消息发送、接收的功能。 Route 路由服务,主要用于客户端鉴权、消息的转发等;提供一些 http 接口,可以用于查看系统状态、在线人数等功能。 当然服务端、路由都可以水平扩展。 这是一个消息发送的流程图,假设现在部署了两个服务端 A、B 和一个路由服务;其中 ClientA 和 ClientB 分别和服务端 A、B 保持了长连接。 当 ClientA 向 ClientB 发送一个 hello world 时,整个的消息流转如图所示: 先通过 http 将消息发...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,CentOS7官方镜像安装Oracle11G
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果