[TOC]
背景
随着数据量和访问量的不断增长,原有的单个数据库的数据存储需要拆分为多个数据库来进行数据的存储,目前的数据拆分方式主要有两种,一种是垂直拆分,一种是水平拆分。垂直拆分就是把一个数据库中不同业务单元的数据分到不同的数据库中,水平拆分是根据一定的规则把同一业务单元的数据拆分到多个数据库中。
目前进行分库分表后,原本一个数据库上的自增id的结果,在分库分表下并不是全局唯一的. 所以,分库分表后需要有一种技术可以生成全局的唯一。
目标
- 生成全局唯一的id;
- 保持高性能;
- 保持高可用
解决思路
- oracle sequence : 基于第三方oracle的SEQ.NEXTVAL来获取一个ID 优势:简单可用 缺点:需要依赖第三方oracle数据库
- mysql id区间隔离 : 不同分库设置不同的起始值和步长,比如2台mysql,就可以设置一台只生成奇数,另一台生成偶数. 或者1台用010亿,另一台用1020亿. 优势:利用mysql自增id 缺点:运维成本比较高,数据扩容时需要重新设置步长.
- 基于数据库更新+内存分配: 在数据库中维护一个ID,获取下一个ID时,会对数据库进行ID=ID+100 WHERE ID=XX,拿到100个ID后,在内存中进行分配 优势:简单高效 缺点:无法保证自增顺序
Sequence可用性
- 只要生成id的数据库不全部挂掉,均可以顺畅提供服务;
- 生成id的数据库数量不定,按照应用对容灾的需求指定不同机器不同机房的数据库; (比如需要考虑单元化多机房的id生成)
- 支持生成id的数据库hang住快速略过和恢复自动加入
原理
目前我们针对多机的id生成方案: 每个数据库只拿自己的那一段id. 如下图,sample_group_0-sample_group_3是我们生成全局唯一id的4个数据库,那么每个数据库对于同一个id有一个起始值,比如间隔是1000.
group ds | value |
---|---|
sample_group_0 | 0 |
sample_group_1 | 1000 |
sample_group_2 | 2000 |
sample_group_3 | 3000 |
应用真正启动的时候,可能某一台机器上去取id,随机取到了sample_group_1,那么这台机器上的应用会拿到1000-1999这一千个id(批量取,这个也就保证了应用端取id性能),而这个时候4个数据库上id起始值会变成下图所示,你也许注意到了,下次从sample_group_1上取得的id就变成了5000-5999. 那么也就是这样,完全避免了多机上取id的重复.比如sample_group_1他会永远只会取到1000-1999,5000-5999,9000-9999, 13000-13999…其他数据库也一样,相互不会重叠.
group ds | value |
---|---|
sample_group_0 | 0 |
sample_group_1 | 5000 |
sample_group_2 | 2000 |
sample_group_3 | 3000 |
TDDL sequence 工作起来是这样的:它会根据 sequence 表里的数据,取 n 个数字放到内存里(n 称为内步长,可以在定义数据源的时候配置),在 n 个数字用完以前,生成的 ID 都从内存里来,用完以后才继续去数据库里取。
举个例子,当表里只有一个条目的时候,内步长为 1000,初值为 0。第一次生成序列的时候,访问数据库,把初值改为 1000,此时可以在内存里依次生成 1 到 1000 这 1000 个数。等到内存里的 1000 个数用完了,再访问数据库,初值变成 2000,内存里开始生成 1001 到 2000 这 1000 个数。这么做应该是为了效率,步长越大,访问数据库的频率就越低。
多个序列(sequence 表里有多个条目)的情况又是怎样呢?这里又涉及到一个外步长的概念:外步长 = 内步长 * sequence 表里条目的数量。刚才说内步长为 1000 的情况下,每次访问库,初值就增加 1000,这个增加的 1000,就是外步长(因为 sequence 表里就一个条目,内外步长相等)。
源码解析
<dependency>
<groupId>com.taobao.tddl</groupId>
<artifactId>tddl-sequence</artifactId>
<version>5.2.1</version>
</dependency>
使用步骤
- 建表
CREATE TABLE `sequence` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`value` bigint(20) NOT NULL,
`gmt_create` timestamp DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` timestamp NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 初始化表里的数据
name:序列的名称,随便取就好,比如叫 seq
value:序列的初值,比如你要从 101 开始生成,就填 100
gmt_modified:不用解释了,填当前的时间就好
- 配置生成器的数据源
<bean id="sequenceDao" class="com.taobao.tddl.client.sequence.impl.GroupSequenceDao" init-method="init">
<!--<bean id="sequenceDao" class="com.taobao.tddl.client.sequence.impl.UnitGroupSequenceDao" init-method="init">单元化用这个-->
<!--appName ,必填 -->
<property name="appName" value="CT_APP_NAME" />
<!--数据源的个数,对应于dbGroupKeys配置的数量 -->
<property name="dscount" value="1" />
<!--dbGroupKeys必填,对应于tddl中的group name -->
<property name="dbGroupKeys">
<list>
<value>APP_GROUP</value>
</list>
</property>
<!--是否开启自适应,一般设为true-->
<!--内步长 -->
<property name="innerStep" value="50" />
<property name="adjust" value="true" />
<property name="retryTimes" value="3" />
<property name="tableName" value="ct_app_sequence"/>
<!--注:在不了解sequence原理的情况下,请勿修改其他诸如innerStep等参数-->
</bean>
<bean id="sequence" class="com.taobao.tddl.client.sequence.impl.GroupSequence" init-method="init">
<property name="sequenceDao" ref="sequenceDao" />
<property name="name" value="commodityId" />
</bean>
这个 bean 的作用是配置上一步建的那个表的来源。appName 和 dbGroupKey 就是 TDDL 的两个 key,可以确定是哪个库,这个大家很熟悉了,不多说。tableName 用来确定是哪个表。用表里的哪些列也是可以配置的,这里就不多说了,官方文档里有。
- 配置生成器
<bean id="sequence" class="com.taobao.tddl.client.sequence.impl.GroupSequence" init-method="init">
<property name="sequenceDao" ref="sequenceDao" />
<property name="name" value="commodityId" />
</bean>
- 使用
拿到生成器的 bean,就可以愉快地使用了。每次调用 nextValue() 的结果每次都不一样,即使在多台机器上也是一样
@Autowired
private Sequence sequence;
...
sequence.nextValue();
注意事项
- 上线前在预发的sequence的值一定要尽量大于线上库
- 他传值的时候没有加##,导致没有把Sequence的值传到SQL里面去,
- 查看数据库里面有没有0-999这样的主键,正如上文所说,Sequence不会生成这样的值;
- 注意到DBA中找TDDL的group配置
- 注意要添加TABLE属性。
网友评论