Vitess全局唯一ID生成的实现方案
为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键。 但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足全局唯一的需求。因此,产生了多种解决方案,如UUID,SnowFlake等。下文将介绍Vitess是如何解决这个问题的。
Vitess全局唯一id生成
在Vitess实现方案中,每个设置了全局唯一列的表,都会对应一张sequence序列表。例如对于表user,会对应一张名为user_seq的序列表,原表与序列表的关联关系会记录在元数据中。user表以及user_seq这两张表元数据信息分别如下:
user表元数据:分片键为name列,分片算法为hash;全局唯一列为id列,依赖user_seq表生成具体的值。
{
"tables": {
"user": {
"column_vindexes": [
{
"column": "name",
"name": "hash"
}
],
"auto_increment": {
"column": "id",
"sequence": "user_seq"
}
}
}
}
user_seq表元数据:表类型标识为sequence。
{
"tables": {
"user_seq": {
"type": "sequence"
}
}
}
所有sequence表表结构相同,如下所示:
CREATE TABLE user_seq (
id int,
next_id bigint,
cache bigint,
PRIMARY KEY (id)
) COMMENT 'vitess_sequence';
且其中只有一条id为0的数据:
mysql> select * from user_seq;
+----+---------+-------+
| id | next_id | cache |
+----+---------+-------+
| 0 | 1000 | 100 |
+----+---------+-------+
sequence表可以认为是一个分号器,cache字段表示每次发放号段的个数,next_id列表示每次发放号段的起始值****。Vitess每个分片在初始化时会从sequence根据next_id、cache获取号段保存在VtTablet(MySQL实例前的代理服务)的内存中,当内存中号段耗尽时,再次从sequence表中获取新号段。
我们深入代码看一下具体的实现逻辑:
// 获取sequence的方法
func (qre *QueryExecutor) execNextval() (*sqltypes.Result, error) {
// 从plan中获取inc(为要获取的id数量)以及tableName
inc, err := resolveNumber(qre.plan.NextCount, qre.bindVars)
tableName := qre.plan.TableName()
t := qre.plan.Table
t.SequenceInfo.Lock()
defer t.SequenceInfo.Unlock()
if t.SequenceInfo.NextVal == 0 || t.SequenceInfo.NextVal+inc > t.SequenceInfo.LastVal {
// 在事务中运行
_, err := qre.execAsTransaction(func(conn *StatefulConnection) (*sqltypes.Result, error) {
// 使用select for update锁住行数据以免在计算并更新新值期间被其他线程修改
query := fmt.Sprintf("select next_id, cache from %s where id = 0 for update", sqlparser.String(tableName))
qr, err := qre.execSQL(conn, query, false)
nextID, err := evalengine.ToInt64(qr.Rows[0][0])
if t.SequenceInfo.LastVal != nextID {
// 如果从_seq表读取得到的id值小于tablet缓存中id,则将缓存中的值更新到_seq表中
if nextID < t.SequenceInfo.LastVal {
log.Warningf("Sequence next ID value %v is below the currently cached max %v, updating it to max", nextID, t.SequenceInfo.LastVal)
nextID = t.SequenceInfo.LastVal
}
t.SequenceInfo.NextVal = nextID
t.SequenceInfo.LastVal = nextID
}
cache, err := evalengine.ToInt64(qr.Rows[0][1])
// 按照cache的倍数获取到大于inc量的缓存,计算出新newLast
newLast := nextID + cache
for newLast < t.SequenceInfo.NextVal+inc {
newLast += cache
}
// 将新的边界值更新到_seq表中
query = fmt.Sprintf("update %s set next_id = %d where id = 0", sqlparser.String(tableName), newLast)
_, err = qre.execSQL(conn, query, false)
t.SequenceInfo.LastVal = newLast
})
}
// 返回获取的sequence值 更新SequenceInfo
ret := t.SequenceInfo.NextVal
t.SequenceInfo.NextVal += inc
return ret
}
从源码中可以看到:
Vitess使用了事务内锁行(
select for update)的方式保证了多线程下查询并更新序列表不会互相干扰。如果VtTablet中自增序列值缓存不足或者号段耗尽后,从sequence表重新获取值,并更新序列表中next_id字段。
根据
inc的大小,即所需ID的数量,VtTablet会以cache为最小块,从序列表中获取n*cache个数量的id缓存在内存中。
补充说明:
1. sequence表为非拆分的表。
2. 全局唯一id生成无法保证连续性。
VtDriver实现方式
在Vitess的SDK客户端方案VtDriver中,sequence的生成逻辑被封装在了MySQL驱动包本身当中,与Vitess的方案类似,对于设置了全局自增的表,其sequence的生成同样依赖于对应的序列表,序列表的结构与Vitess的序列表相同(参上),但是读取并更新字段next_id的方式使用了CAS的方案:
public long[] querySequenceValue(Vcursor vCursor, ResolvedShard resolvedShard, String sequenceTableName) throws SQLException, InterruptedException {
// cas 重试次数限制
int retryTimes = DEFAULT_RETRY_TIMES;
while (retryTimes > 0) {
// 查询_seq表中的sequence设置,其中cache为本地缓存的大小
String querySql = "select next_id, cache from " + sequenceTableName + " where id = 0";
VtResultSet vtResultSet = (VtResultSet) vCursor.executeStandalone(querySql, new HashMap<>(), resolvedShard, false);
long[] sequenceInfo = getVtResultValue(vtResultSet);
long next = sequenceInfo[0];
long cache = sequenceInfo[1];
// 将计算出的next_id的值尝试更新到_seq表中,如果失败则重新读取并更新,直到成功为止
String updateSql = "update " + sequenceTableName + " set next_id = " + (next + cache) + " where next_id =" + sequenceInfo[0];
VtRowList vtRowList = vCursor.executeStandalone(updateSql, new HashMap<>(), resolvedShard, false);
if (vtRowList.getRowsAffected() == 1) {
sequenceInfo[0] = next;
return sequenceInfo;
}
retryTimes--;
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 6));
}
throw new SQLException("Update sequence cache failed within retryTimes: " + DEFAULT_RETRY_TIMES);
}
在源码中可以看到:
在整个查询并更新序列表的过程中,没有出现Vitess实现中的开启事务以及产生锁表的情况,而是使用了CAS更新的方式。
利用
update user_seq set next_id=? where next_id=?执行的返回值判断是否语句是否更新成功,如果失败则重新查询next_id的值,计算新值再尝试更新, 如果出现并发争抢的情况,Vtdriver中允许最多的重试次数DEFAULT_RETRY_TIMES为100次。
VtDriver中使用sequence的方式与MySQL自增键类似,如果设置了sequence的表在插入数据的过程中,自增列没有给定具体的值,会直接从本地缓存中获取自增ID,如果无缓存或者缓存不足时,才会路由到序列表所在MySQL服务获取sequence值。
事务+锁表 or CAS ?
在Vitess实现sequence的源码当中,其更新序列表的过程为:开启事务时执行select for update,使用表锁,保证多线程安全。在现实往往充满了不确定性,我们可以想象一下:如果应用锁了数据库中的表后,由于自身的性能原因等而迟迟没有执行commit操作,或者应用节点出现了宕机的情况,此时:
应用宕机后,其持有的锁不会被释放!后续任何其他连接对于该表的任何SQL都会被持续阻塞!
VtDriver作为Vitess的客户端方案,如果其sequence实现采用事务锁的方式,由于各个应用端都会与MySQL服务直连,即各个应用获取sequence的过程都会产生锁表的行为。此时,一旦应用端由于某些原因出现锁表时长增大,甚至于应用宕机的情况,则所有应用都会由于其锁表而产生非常明显的性能下降甚至死锁。采用cas的方式使得整个过程不需要显式的开启事务,不需要锁行,自然也不存在潜在的死锁风险。当然,CAS在并发高于一定程度时会出现各个线程互相争抢资源,此时会有更新失败不断重试的情况发生,给CPU带来一定的压力,而这可以通过设置更大的cache值,增加本地缓存数量的方式来调节。
作者:京东零售 金越
来源:京东云开发者社区 转载请注明来源
Vitess全局唯一ID生成的实现方案的更多相关文章
- (4.24)【mysql、sql server】分布式全局唯一ID生成方案
参考:分布式全局唯一ID生成方案:https://blog.csdn.net/linzhiqiang0316/article/details/80425437 分表生成唯一ID方案 sql serve ...
- 关于全局唯一ID生成方法
引:最近业务开发过程中需要涉及到全局唯一ID生成.之前零零总总的收集过一些相关资料,特此整理以便后用 本博客已经迁移至:http://cenalulu.github.io/ 本篇博文已经迁移,阅读全文 ...
- 【Redis场景拓展】秒杀问题-全局唯一ID生成策略
全局唯一ID 为什么要使用全局唯一ID: 当用户抢购时,就会生成订单并保存到订单表中,而订单表如果使用数据库自增ID就存在一些问题: 受单表数据量的限制 id的规律性太明显 场景分析一:如果我们的id ...
- 分布式系统全局唯一ID生成
一 什么是分布式系统唯一ID 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识. 如在金融.电商.支付.等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息, ...
- 分布式全局唯一ID生成策略
为什么分布式系统需要用到ID生成系统 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识.如在美团点评的金融.支付.餐饮.酒店.猫眼电影等产品的系统中,数据日渐增长,对数据库的分库分表后需要有 ...
- mysql全局唯一ID生成方案(二)
MySQL数据表结构中,一般情况下,都会定义一个具有‘AUTO_INCREMENT’扩展属性的‘ID’字段,以确保数据表的每一条记录都可以用这个ID唯一确定: 随着数据的不断扩张,为了提高数据库查询性 ...
- 常见分布式全局唯一ID生成策略
全局唯一的 ID 几乎是所有系统都会遇到的刚需.这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义.工业上有多种策略来获取这个全局唯一的id,针对常见的几种场景,我在这里进行 ...
- 分布式全局唯一ID生成策略
一.背景 分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表.因为数据量巨大一张表无法承接,就会对其进行分库分表. 但一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题. ...
- Snowflake 全局唯一Id 生成
/// <summary> /// From: https://github.com/twitter/snowflake /// An object that generates IDs. ...
- Twitter全局唯一ID生成算法
测试:private static void TestIdWorker() { HashSet<long> set = new HashSet<long>(); IdWorke ...
随机推荐
- Python-Loguru:让记录日志更装13
" 今天勇哥来介绍一款让日志记录在 Python 中变得更加轻松愉快的库--Loguru.它提供了强大的功能和简洁的接口,使我们能够以更加灵活和直观的方式记录和管理日志信息,据说比loggi ...
- NOIP模拟测试A3 赛后总结
T1 谜之阶乘 可以发现题目要求我们求的实际上是若干个连续整数 \(c_i\) ,使得 \(\displaystyle \prod c_i = n\),通过打表可以发现这些连续整数的长度 \(d\) ...
- spring连接数据库mysql报错 state 08S01 com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
spring连接数据库mysql报错errorCode0,state08S01com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Comm ...
- 三路快排Java版(图文并茂思路分析)
快速排序 这里我们直接开始讲相对的最优解 带随机数的三路快排 好了,中间还有很多版本的快排,但是都有一些问题导致在某种极端情况下造成耗费时间极多. 基础快排:在序列本身有序的情况下复杂度为O(n²) ...
- ASIC加速技术原理与实践:从芯片设计到优化
目录 <ASIC加速技术原理与实践:从芯片设计到优化> 背景介绍: 随着数字电路技术的不断发展,ASIC(专门芯片)作为数字电路中的核心部分,逐渐成为芯片设计中的重要组成部分.ASIC加速 ...
- 【笔试实战】LeetCode题单刷题-编程基础 0 到 1【三】
682. 棒球比赛 题目链接 682. 棒球比赛 题目描述 你现在是一场采用特殊赛制棒球比赛的记录员.这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分. 比赛开始时,记录是空白的.你 ...
- Java中AQS的原理与实现
目录 1:什么是AQS? 2:AQS都有那些用途? 3:我们如何使用AQS 4:AQS的实现原理 5:对AQS的设计与实现的一些思考 1:什么是AQS 随着计算机的算力越来越强大,各种各样的并行编 ...
- PTA 21级数据结构与算法实验5—树和二叉树
目录 7-1 还原二叉树 7-2 朋友圈 7-3 修理牧场 7-4 玩转二叉树 7-5 根据后序和中序遍历输出先序遍历 7-6 完全二叉树的层序遍历 7-7 列出叶结点 7-8 部落 7-9 建立与遍 ...
- PlayWright(二十二)- allure插件(一)
在上文中,我们介绍并使用了pytest-html插件,总之并不复杂,但是今天我们要讲一个比pytest-html插件强很多的插件allure报告,我们要掌握他并且灵活使用,之后的框架就不需要考虑其他的 ...
- Python 潮流周刊第 13 期(2023-07-29)
查看全文: https://pythoncat.top/posts/2023-07-29-weekly 文章&教程 1.Jupyter Notebook 7 隆重发布 (英) 2.Python ...