分库分表产生的4大问题:跨库问题、分布式事务问题、查询数据结果集合并、全局唯一性id保证。本文掰扯下全局唯一性id。
全局唯一id的4种生成策略:UUID、数据库递增、snowflake、Redis
全局唯一性id保证的4个要求:全局唯一、数据递增、信息安全、高并发高可用
---a.全局唯一:不能出现重复的id号;
---b.数据递增:保证下一个id一定大于上一个id;
---c.信息安全:防止恶意用户根据id规则来获取数据;
---d.高并发高可用:短时间内快速生成可用,解决线程安全问题。。
分布式唯一id生成策略 | 优点 | 缺点 |
UUID(通用唯一识别码) | 代码实现简单、不占用带宽、数据迁移不受影响 | 无序无法建索引、无法保证数据趋势递增、字符存储,传输和查询慢、不可读 |
snowflake雪花算法 | 代码实现简单、不占用带宽、数据迁移不受影响、低位趋势递增 | 无序无法建索引、无法保证数据趋势递增、强依赖时钟(多台服务器时间一定要一样) |
数据库递增 | 代码实现简单、性能好、数字排序、可读性强 | 位数不确定,有溢出风险、高并发情况下有性能瓶颈、受限数据库、扩展麻烦、插入数据库才能拿到id、单点故障问题 |
redis | 不依赖数据库、灵活方便、性能优于数据库、没有单点故障(高可用) | 需要占用网络资源、性能要比本地生成慢、需要增加插件 |
UUID是通用唯一识别码。16B=128bit的长数字。组成=当前日期和时间序列+全局唯一性网卡mac地址。java可使用java.util.UUID来做。
优点:代码实现简单、不占用带宽、数据迁移不受影响
缺点:无序、无法保证数据趋势递增、字符存储,传输和查询慢、不可读
uuid有很多版本,比如:MD5,SHA1,随机
二、snowflake雪花算法(国外twitter)
文章图片
1bit高位随机 + 41bit毫秒数 + 10bit机器码(数据中心+机器id) + 12bit序列号 = 64bit
1bit:二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0;
41bit:可表示2^41?1个毫秒的值。转化成单位年则是(2^41?1)/(1000*60*60*24*365)=69年;
10bit:用来记录工作机器id。可以部署在2^10=1024个节点,包括5位datacenterId和5位workerId,5bit可以表示的最大正整数是2^5?1=31,即可以用0、1、2、3....31这32个数字,来表示不同的datecenterId或workerId;
12bit:序列号,用来记录同毫秒内产生的不同id。12bit可表示的最大正整数是2^12?1=4095,表示同一机器同一时间截(毫秒)内产生的4095个ID序号;
SnowFlake可以:每毫秒最多生成4096个ID,每秒可达4096000个(每秒400万+个ID)。理论上,只要CPU计算能力足够,单机每秒实测10w+:snowflake每秒能够产生26万个ID; 所有生成的id按时间趋势递增; 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)详见:https://github.com/souyunku/SnowFlake
国内保证数据唯一性就行了---IDC机房
优点:代码实现简单、不占用带宽、数据迁移不受影响、低位趋势递增
缺点:无序、无法保证数据趋势递增、强依赖时钟(多台服务器时间一定要一样)
关键代码如下,详细代码见附录
snowflake算法生成id的关键代码: 文章图片 ---------------------------------------------------------------- 需要解析生成的id,可以使用如下方式: 文章图片 其中,arr[0-3]分别代表id生成的时间戳,机器标识,业务标识,序列号 |
//Snowflake生成订单号需要注意的是其中两个参数的含义 //dataCenterId可用于区分机器,workerId可用于区分业务 //datacenterId=2; workerId=5 Snowflake snowflake = new Snowflake(2, 5); long id1 = snowflake.nextId(); long id2 = snowflake.nextId(); |
三、数据库递增(mysql自增id)
可以设置步长,比如:奇偶,递增步长=2
适合小型互联网公司,比如:5w订单量,一年1800w,mysql一张表500w,如果公司每天订单量5w的数据,用mysql设置步长=100(100张表),可以用27年,如果公司拿到风投了,每天的订单量100w,撑不过3年!
文章图片 |
文章图片 |
CREATE TABLE `tl_num`(
`id` bigint(11) NOT NULL AUTO INCREMENT,
KEY (`id`) USING BTREE
) ENGINE=InnoDB auto increment=1 DEFAULT CHARSET=utf8;
优点:代码实现简单、性能好、数字排序、可读性强
缺点:受限数据库、扩展麻烦、插入数据库才能拿到id、单点故障问题
主从同步的时候:电商下单--->支付insert master db select数据,因为数据同步延迟导致查不到这个数据。解决:加cache(不是最好的方式),数据要求比较严的话查master主库。
四、redis
缩减版本、有关业务代码没有包含到里头、Redis方案
文章图片
优点:不依赖数据库、灵活方便、性能优于数据库、没有单点故障(高可用)
缺点:需要占用网络资源、性能要比本地生成慢、需要增加插件
【分布式|分布式ID生成器-订单号的生成(全局唯一id生成策略)】
=================附录============================================
snowflake第一种解法:https://github.com/souyunku/SnowFlake
实测:100万个ID 耗时5秒/**
* 描述: Twitter的分布式自增ID雪花算法snowflake (Java版)
**/
public class SnowFlake {/**
* 起始的时间戳
*/
private final static long START_STMP = 1480166465631L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12;
//序列号占用的位数
private final static long MACHINE_BIT = 5;
//机器标识占用的位数
private final static long DATACENTER_BIT = 5;
//数据中心占用的位数/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId;
//数据中心
private long machineId;
//机器标识
private long sequence = 0L;
//序列号
private long lastStmp = -1L;
//上一次时间戳public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards.Refusing to generate id");
}if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT//数据中心部分
| machineId << MACHINE_LEFT//机器标识部分
| sequence;
//序列号部分
}private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}private long getNewstmp() {
return System.currentTimeMillis();
}public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
long start = System.currentTimeMillis();
for (int i = 0;
i < 1000000;
i++) {
System.out.println(snowFlake.nextId());
}System.out.println(System.currentTimeMillis() - start);
}
}
snowflake第二种解法:
/** * Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 加起来刚好64位,为一个Long型。
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
=
public class Snowflake {
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1489111610226L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long dataCenterIdBits = 5L;
/** 支持的最大机器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;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
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;
private static SnowflakeIdWorker idWorker;
static {
idWorker = new SnowflakeIdWorker(1,1);
}/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long dataCenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
}this.workerId = workerId;
this.dataCenterId = dataCenterId;
}/** 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
throw new RuntimeException(String.format("Clock moved backwards.Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {//时间戳改变,毫秒内序列重置
sequence = 0L;
}
lastTimestamp = timestamp;
//上次生成ID的时间截
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)| (dataCenterId << dataCenterIdShift)| (workerId << workerIdShift)| sequence;
}/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}/**
* 静态工具类
* @return
*/
public static Long generateId(){
long id = idWorker.nextId();
return id;
}/** 测试 */
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
long startTime = System.nanoTime();
for (int i = 0;
i < 50000;
i++) {
long id = SnowflakeIdWorker.generateId();
System.out.println(id);
}
System.out.println((System.nanoTime()-startTime)/1000000+"ms");
}}
推荐阅读
- 数据库|【分布式系列01期】常见的分布式ID生成方案浅析及大厂方案调研
- 分布式|全网最全的分布式ID生成方案解析
- 分布式|常用唯一ID生成方案分析(从单机到分布式)
- SpringBoot|第四十章(基于SpringBoot & Quartz完成定时任务分布式多节点负载持久化)
- 历史上的今天|【历史上的今天】3 月 3 日(AT&T 成立;全球最大分布式计算项目正式停止;家酿俱乐部首次会议)
- java|四种常用的微服务架构拆分方式
- 分布式|什么是云原生,跟云计算有什么关系(终于有人讲明白了)
- java|云原生解决什么问题()
- 小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁