第三节:分库分表下订单ID的生成的几种方案
一. 背景
主流架构一般分库分表都会涉及,追求性能的同时,带来各种痛点。
比如订单id的生成,在分表的情况下,使用int自增,两张分表都自增,直接会导致主键ID的重复,这是错误的,本节主要就是解决分库分表情况下Id的生成规则。
下面先补充一下常见的分库分表中间件:
1. DB层次的,针对DB做代理。
ShardingSphere-Proxy的架构图
2. 代码层次的
最常见的是 sharding-jdbc (他和 shardingsphere-proxy是同一产品下的东西,只是作用的层次不同),如下图:
二者的区别详见:https://shardingsphere.apache.org/document/legacy/3.x/document/cn/overview/
二. 方案剖析
1. 自增方案
方案1:起始点分段
比如设置分表2的起始点从10000开始
ALTER TABLE incorder_1 AUTO_INCREMENT=10000;
优缺点:
简单容易,数据库层面设置,代码是不需要动的
边界的切分人为维护,操作复杂,触发器自动维护可以实现但在高并发下不推荐
方案2:分段步长的自增
--查看
show session variables like 'auto_inc%';
show global variables like 'auto_inc%';
--设定自增步长
set session auto_increment_increment=2;
--设置起始值
set session auto_increment_offset=1;
--全局的
set global auto_increment_increment=2;
set global auto_increment_offset=1;
分析:
影响范围不可控,要么session每次设置,忘记会出乱子。要么全局设置,影响全库所有表
结论:不可取!!!
方案3:Sequence特性
仅限于oracle和sqlserver,主流mysql不支持
-- 创建一个sequence:
create sequence sequence_name as int minvalue 1 maxvalue 1000000000 start with 1
increment by 1 no cache;
-- sequence的使用:
sequence_name.nextval
sequence_name.currval
-- 在表中使用sequence:
insert into incorder_0 values(sequence_name.nextval,userid);
2. 基于业务规则自定义生成
不用自增,自定义id,加上业务属性,从业务细分角度对并发性降维。例如淘宝,在订单号中加入用户id。
加上用户id后,并发性维度降低到单个用户,每个用户的下单速度变的可控。
时间戳+userid,业务角度,一个正常用户不可能1毫秒内下两个单子,即便有说明是刻意刷单,应该被前端限流。
[HttpPost]
public IActionResult CreateIdWay1()
{
//时间戳+userId
// 转换为毫秒时间戳
long unixTimestampMilliseconds = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds;
string userId = "user001";
string orderId = unixTimestampMilliseconds + "_" + userId;
return Json(new { status = "ok", msg = "获取成功", data = orderId });
}
3. 集中式分配
方案1-MaxId表 (了解思路即可)
(1) 创建一张maxId表
CREATE TABLE `maxid` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`nextid` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
);
insert into maxid(name,nextid) values ('myOrderId',1000);
(2) 创建一个函数,方便调用,生成orderId
DROP FUNCTION IF EXISTS getid;
-- 创建函数
CREATE FUNCTION getid(myOrderId VARCHAR(50))
RETURNS BIGINT(20)
BEGIN
-- 定义变量
DECLARE id BIGINT(20);
-- 给定义的变量赋值
UPDATE maxid SET nextid = nextid + 1 WHERE name = myOrderId;
SELECT nextid INTO id FROM maxid WHERE name = myOrderId;
-- 返回函数处理结果
RETURN id;
END;
(3) 调用函数,获取orderId
SELECT getid('myOrderId') from dual
(4). 通过程序代码调用getId函数即可
(省略)
方案2-redis缓存 【推荐】
利用redis的单线程、原子性、自增性来生成id使用。
[HttpPost]
public IActionResult CreateIdWay3()
{
long orderId = RedisHelper.IncrBy("orderId");
return Json(new { status = "ok", msg = "获取成功", data = orderId });
}
分析:
需要额外的中间件redis
与db相比不够直观,不方便查看当前增长的id值,需要额外连接redis服务器读取
性能不是问题,redis得到业界验证和认可
对redis集群的可靠性要求很高,禁止出现故障,否则全部入库被阻断
数据一致性需要注意,尽管redis有持久策略,down机恢复时需要确认和当前库中最大id的一致性
4. uuid和guid
java中uuid
public Strorder uuid(int userid){
Strorder order = new Strorder();
order.setId(UUID.randomUUID().toString());
order.setUserid(userid);
strorderMapper.save(order);
return order;
}
.Net中Guid
[HttpPost]
public IActionResult CreateIdWay4()
{
var orderId0 = Guid.NewGuid();
string orderId1 = Guid.NewGuid().ToString();
string orderId2 = Guid.NewGuid().ToString("N");
return Json(new { status = "ok", msg = "获取成功", data = new { orderId0, orderId1, orderId2 } });
}
分析:
最简单的方案,数据迁移方便
缺点也是非常明显的,太过冗长,非常的不友好,可读性极差
需要使用字符串存储,占用大量存储空间
在建立索引和基于索引进行查询时性能不如数字
5. 雪花算法【推荐!!】
详见下面
三. 雪花算法
1. 简介
UUID 能保证时空唯一,但是过长且是字符,雪花算法由Twitter发明,是一串数字。
Snowflake是一种约定,它把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个64bits 的整数 ID,能够使用 (2^41)/(1000*60*60*24*365) = 69.7 年,每台机器每毫秒理论最多生成 2^12 个 ID
1 bit:固定为0
二进制里第一个bit如果是 1,表示负数,但是我们生成的 id都是正数,所以第一个 bit 统一都是 0。
41 bit:时间戳,单位毫秒
41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值。
注意:这个时间不是绝对时间戳,而是相对值,所以需要定义一个系统开始上线的起始时间
10 bit:哪台机器产生的
代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。官方定义,前5 个 bit 代表机房 id,后5 个 bit 代表机器 id。这10位是机器维度,可以根据公司的实际情况自由定制。
12 bit:自增序列
同1毫秒内,同一机器,可以产生2 ^ 12 - 1 = 4096个不同的 id。
优缺点:
不依赖第三方介质例如 Redis、数据库,本地程序生成分布式自增 ID
只能保证在工作组中的机器生成的 ID 唯一,不同组下可能会重复
时间回拨后,生成的 ID 就会重复,所以需要保持时间是网络同步的。
2. 实操
代码实现
using System; /// <summary> /// 雪花算法 /// 已经考虑了多线程问题 和 时钟回拨问题 /// </summary> public class Snowflake { private static readonly object lockObj = new object(); private long workerId; private long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; private long maxWorkerId = -1L ^ (-1L << 5); private long maxDatacenterId = -1L ^ (-1L << 5); private long sequenceMask = -1L ^ (-1L << 12); private static readonly long twepoch = DateTime.Parse("2021-01-01").Ticks; public Snowflake(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) throw new ArgumentException($"worker Id can't be greater than {maxWorkerId} or less than 0"); if (datacenterId > maxDatacenterId || datacenterId < 0) throw new ArgumentException($"datacenter Id can't be greater than {maxDatacenterId} or less than 0"); this.workerId = workerId; this.datacenterId = datacenterId; } public long NextId() { lock (lockObj) { long timestamp = GetTimestamp(); if (timestamp < lastTimestamp) throw new Exception($"Clock moved backwards. Refusing to generate id for {lastTimestamp - timestamp} milliseconds"); if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) timestamp = TilNextMillis(lastTimestamp); } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - twepoch) << 22) | (datacenterId << 17) | (workerId << 12) | sequence; } } private long TilNextMillis(long lastTimestamp) { var timestamp = GetTimestamp(); while (timestamp <= lastTimestamp) { timestamp = GetTimestamp(); } return timestamp; } private long GetTimestamp() { return DateTime.UtcNow.Ticks - twepoch; } } class Program { static void Main(string[] args) { var snowflake = new Snowflake(workerId: 1, datacenterId: 1); for (int i = 0; i < 10; i++) { long id = snowflake.NextId(); Console.WriteLine($"Generated ID: {id}"); } } }
调用
[HttpPost]
public IActionResult CreateIdWay5()
{
var snowflake = new Snowflake(workerId: 1, datacenterId: 1);
long orderId = snowflake.NextId();
return Json(new { status = "ok", msg = "获取成功", data = orderId });
}
返回结果
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。