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 }
从源码中可以看到:
select for update
)的方式保证了多线程下查询并更新序列表不会互相干扰。 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); }
在源码中可以看到:
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值,增加本地缓存数量的方式来调节。
作者:京东零售 金越
来源:京东云开发者社区 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
代码层面探索前端性能 | 京东云技术团队
前言 最近在做性能优化,具体优化手段,网上铺天盖地,这里就不重复了。 性能优化可分为以下几个维度:代码层面、构建层面、网络层面。 本文主要是从代码层面探索前端性能,主要分为以下 4 个小节。 使用 CSS 替代 JS 深度剖析 JS 前端算法 计算机底层 使用 CSS 替代 JS 这里主要从动画和 CSS 组件两个方面介绍。 CSS 动画 CSS2 出来之前,哪怕要实现一个很简单的动画,都要通过 JS 实现。比如下面红色方块的水平移动: 对应 JS 代码: let redBox = document.getElementById('redBox') let l = 10 setInterval(() => { l+=3 redBox.style.left = `${l}px` }, 50) 1998 年的 CSS2 规范,定义了一些动画属性,但由于受当时浏览器技术限制,这些特性并没有得到广泛的支持和应用。 直到 CSS3 的推出,CSS 动画得到了更全面地支持。同时,CSS3 还引入了更多的动画效果,使得 CSS 动画在今天的 Web 开发中得到了广泛的应...
- 下一篇
MySQL事务死锁问题排查 | 京东云技术团队
一、背景 在预发环境中,由消息驱动最终触发执行事务来写库存,但是导致MySQL发生死锁,写库存失败。 com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: rpc error: code = Aborted desc = Deadlock found when trying to get lock; try restarting transaction (errno 1213) (sqlstate 40001) (CallerID: ): Sql: "/* uag::omni_stock_rw;xx.xx.xx.xx:xxxxx;xx.xx.xx.xx:xxxxx;xx.xx.xx.xx:xxxxx;enable */ insert into stock_info(tenant_id, sku_id, store_id, available_num, actual_good_num, order_num, created, modified, SAVE_VERSION, stock_id) values (...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能