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表生成具体的值。

1{ 2 "tables": { 3 "user": { 4 "column_vindexes": [ 5 { 6 "column": "name", 7 "name": "hash" 8 } 9 ], 10 "auto_increment": { 11 "column": "id", 12 "sequence": "user_seq" 13 } 14 } 15 } 16}

user_seq表元数据:表类型标识为sequence。

1{ 2 "tables": { 3 "user_seq": { 4 "type": "sequence" 5 } 6 } 7}

所有sequence表表结构相同,如下所示:

1CREATE TABLE user_seq ( 2 id int, 3 next_id bigint, 4 cache bigint, 5 PRIMARY KEY (id) 6) COMMENT 'vitess_sequence';

且其中只有一条id为0的数据:

1mysql> select * from user_seq; 2+----+---------+-------+ 3| id | next_id | cache | 4+----+---------+-------+ 5| 0 | 1000 | 100 | 6+----+---------+-------+

sequence表可以认为是一个分号器,cache字段表示每次发放号段的个数,next_id列表示每次发放号段的起始值**。**Vitess每个分片在初始化时会从sequence根据next_id、cache获取号段保存在VtTablet(MySQL实例前的代理服务)的内存中,当内存中号段耗尽时,再次从sequence表中获取新号段。

我们深入代码看一下具体的实现逻辑:

1// 获取sequence的方法 2func (qre *QueryExecutor) execNextval() (*sqltypes.Result, error) { 3 // 从plan中获取inc(为要获取的id数量)以及tableName 4 inc, err := resolveNumber(qre.plan.NextCount, qre.bindVars) 5 tableName := qre.plan.TableName() 6 t := qre.plan.Table 7 t.SequenceInfo.Lock() 8 defer t.SequenceInfo.Unlock() 9 if t.SequenceInfo.NextVal == 0 || t.SequenceInfo.NextVal+inc > t.SequenceInfo.LastVal { 10 // 在事务中运行 11 _, err := qre.execAsTransaction(func(conn *StatefulConnection) (*sqltypes.Result, error) { 12 // 使用select for update锁住行数据以免在计算并更新新值期间被其他线程修改 13 query := fmt.Sprintf("select next_id, cache from %s where id = 0 for update", sqlparser.String(tableName)) 14 qr, err := qre.execSQL(conn, query, false) 15 nextID, err := evalengine.ToInt64(qr.Rows[0][0]) 16 17 if t.SequenceInfo.LastVal != nextID { 18 // 如果从_seq表读取得到的id值小于tablet缓存中id,则将缓存中的值更新到_seq表中 19 if nextID < t.SequenceInfo.LastVal { 20 log.Warningf("Sequence next ID value %v is below the currently cached max %v, updating it to max", nextID, t.SequenceInfo.LastVal) 21 nextID = t.SequenceInfo.LastVal 22 } 23 t.SequenceInfo.NextVal = nextID 24 t.SequenceInfo.LastVal = nextID 25 } 26 cache, err := evalengine.ToInt64(qr.Rows[0][1]) 27 28 // 按照cache的倍数获取到大于inc量的缓存,计算出新newLast 29 newLast := nextID + cache 30 for newLast < t.SequenceInfo.NextVal+inc { 31 newLast += cache 32 } 33 // 将新的边界值更新到_seq表中 34 query = fmt.Sprintf("update %s set next_id = %d where id = 0", sqlparser.String(tableName), newLast) 35 _, err = qre.execSQL(conn, query, false) 36 t.SequenceInfo.LastVal = newLast 37 }) 38 } 39 // 返回获取的sequence值 更新SequenceInfo 40 ret := t.SequenceInfo.NextVal 41 t.SequenceInfo.NextVal += inc 42 return ret 43}

从源码中可以看到:

  1. Vitess使用了事务内锁行(select for update)的方式保证了多线程下查询并更新序列表不会互相干扰。

  2. 如果VtTablet中自增序列值缓存不足或者号段耗尽后,从sequence表重新获取值,并更新序列表中next_id字段。

  3. 根据inc的大小,即所需ID的数量,VtTablet会以cache为最小块,从序列表中获取n*cache个数量的id缓存在内存中。

补充说明:

1. sequence表为非拆分的表。

2. 全局唯一id生成无法保证连续性。

VtDriver实现方式

在Vitess的SDK客户端方案VtDriver中,sequence的生成逻辑被封装在了MySQL驱动包本身当中,与Vitess的方案类似,对于设置了全局自增的表,其sequence的生成同样依赖于对应的序列表,序列表的结构与Vitess的序列表相同(参上),但是读取并更新字段next_id的方式使用了CAS的方案:

1public long[] querySequenceValue(Vcursor vCursor, ResolvedShard resolvedShard, String sequenceTableName) throws SQLException, InterruptedException { 2 // cas 重试次数限制 3 int retryTimes = DEFAULT_RETRY_TIMES; 4 while (retryTimes > 0) { 5 // 查询_seq表中的sequence设置,其中cache为本地缓存的大小 6 String querySql = "select next_id, cache from " + sequenceTableName + " where id = 0"; 7 VtResultSet vtResultSet = (VtResultSet) vCursor.executeStandalone(querySql, new HashMap<>(), resolvedShard, false); 8 long[] sequenceInfo = getVtResultValue(vtResultSet); 9 long next = sequenceInfo[0]; 10 long cache = sequenceInfo[1]; 11 12 // 将计算出的next_id的值尝试更新到_seq表中,如果失败则重新读取并更新,直到成功为止 13 String updateSql = "update " + sequenceTableName + " set next_id = " + (next + cache) + " where next_id =" + sequenceInfo[0]; 14 VtRowList vtRowList = vCursor.executeStandalone(updateSql, new HashMap<>(), resolvedShard, false); 15 if (vtRowList.getRowsAffected() == 1) { 16 sequenceInfo[0] = next; 17 return sequenceInfo; 18 } 19 retryTimes--; 20 Thread.sleep(ThreadLocalRandom.current().nextInt(1, 6)); 21 } 22 throw new SQLException("Update sequence cache failed within retryTimes: " + DEFAULT_RETRY_TIMES); 23}

在源码中可以看到:

  1. 在整个查询并更新序列表的过程中,没有出现Vitess实现中的开启事务以及产生锁表的情况,而是使用了CAS更新的方式。

  2. 利用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值,增加本地缓存数量的方式来调节。

作者:京东零售 金越

来源:京东云开发者社区 转载请注明来源

点赞
收藏

评论区

加载中...

相关推荐

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

如何设计一款“高可用高性能”的发号器

本文已收录https://github.com/lkxiaolou/lkxiaolou欢迎star。背景在分布式场景中,很多地方需要生成全局唯一的id,如数据库分库分表后需要用唯一id代替单机版本的自增id。发号器的基本要求是全局唯一,无论如何都不能重复某些场景下还要求单调递增,如排序需求等。网上有很多介绍发号器的文章,比如美团的《Leaf——美团点

MySQL使用on duplicate key update时导致主键不连续自增

使用onduplicatekeyupdate语法有时是很方便,但是会有一个影响:默认情况下,每次更新都会更新该表的自增主键ID,如果更新频率很快,会导致主键ID自增的很快,过段时间就超过数字类型的的范围了解决这个问题,有两种方式:(实际我目前使用的方式是把自增主键ID设置为bigint,也有一部分操作先查询再选择插入OR更新)方法一:拆分成两个

mysql自增id获取

mysql自增id获取使用max函数:selectmax(id)fromtablename 优点:使用方便快捷。缺点:获取的不是真正的自增id,是表中最大的Id,如果有删除数据的话,那么该值和自增id相差比较大。如果有连表数据,有可能导致数据错乱。使用LAST\_INSERT\_ID

Twitter的分布式自增ID算法snowflake (Java版)

概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移

mysql关于自动编号问题 转载

MySql数据库唯一编号字段(自动编号字段)在数据库应用,我们经常要用到唯一编号,以标识记录。在MySQL中可通过数据列的AUTO\_INCREMENT属性来自动生成。MySQL支持多种数据表,每种数据表的自增属性都有差异,这里将介绍各种数据表里的数据列自增属性。ISAM表如果把一个NULL插入到一个AUTO\_INCREMEN