背景
Oracle数据库提供了sequence的特性,即用户可以通过create sequence seq语法创建一个sequence对象,用来生成一个单机或者RAC环境下,唯一,单调递增/递减,
全局有序(RAC环境)的数字,极大的简化了应用的设计,满足应用对全局唯一的需求。
然而, MySQL 并没有sequence对象类型的存在,为满足用户使用需求, RDS for MySQL移植了Oracle的sequence特性。
语法支持
RDS
for MySQL尽可能的兼容Oracle的sequence语法,
但还是会有一些细微的差别:
1、创建语法
create sequence seq_1
start with 1
minvalue 1
maxvalue 1000000000
increment by 1
cache 10
cycle;
和Oracle的创建sequence语法有一个clause的差别,就是MySQL不支持order clause,原因是MySQL只支持在单机情况下的全局有序,无法在分布式环境下实现,所以,默认在单机环境下就是order的,无需指定,也不支持设置。
2、获取sequence语法
select nextval from seq_1;
select
currval from seq_1;
MySQL原生没有支持dual表,所以获取序列值的方法,沿用了普通的select语法,语法处理起来也比较简单。
设计思路
1.创建一张表表示sequence对象
MySQL使用InnoDB引擎的一张普通的表来表示sequence对象,固定sequence的columns是:
(currval, nextval, start, minvalue,
maxvalue, increment, cache, cycle, used)
其中:
start,
minvalue, maxvalue, increment, cache, cycle
这些字段对应着create sequence语法中指定的值,如果没有指定会有默认值存在。
used
used字段为了区分第一个nextval,如果第一次取是start值,如果超过maxvalue,并且支持cycle,会从minvalue重新开始。
currval, nextval
这两个字段会在select nextval from seq_1执行后,根据cache定义的缓存使用完后,推高其大小。
2.初始化一条记录,表示sequence的值
在创建sequence对象的时候,初始化一条记录(有且仅有一条),记录表示了sequence的定义。
used=0, 表示nextval,currval还没有初始化过。
3.定义缓存
在MySQL server层,在table_share(表定义)对象中,缓存一份Sequence定义和currval,nextval,用来提供cache功能。
4.读取流程
读取select nextval from seq逻辑,参考下面的流程图:
一共包含两条路径
路径1:当sequence cache中缓存的内容还没有用完的时候,通过步长increment直接递增nextval,返回结果集给用户。
路径2:当sequence cache中缓存的内容用完的时候,需要推高currval,然后更新sequence表中的记录,最后置换Sequence cache中的缓存,并递增nextval,返回结果集给用户。
常见的两个问题
问题1:sequence 连续性问题
因为cache的原因,实例crash,就会带来nextval的连续性的问题,但sequence的特性本没有支持这个特性,只保留唯一性和单调有序。
问题2:sequence性能问题
sequence的查询,如果cache命中,性能开销基本可以忽略,如果cache未命中,相当update+commit语句的代价,需要性能的问题,依赖用户定义的cache大小。
sequence查询的主流层,就是处理缓存的一般逻辑,看起来并没有多少复杂度,但代码上需要处理的问题,因为事务系统的原因,持久化的操作变得相对比较复杂:
RDS移植sequence解决的问题点
1.
sequence对象类型持久化
Sequence对象虽然底层是使用一张InnoDB表来模拟的,但需要持久化这个类型,以保证系统在读取的时候,识别出来这是一个sequence,我们采用了类似view(视图)的的保存策略,
在MySQL的frm文件中,使用了保留位head[39]=SEQUENCE_TABLE来表示这是一个sequence表。
2.
sequence对象的初始化
在create sequence的过程中,包含了两块处理逻辑:
(1)
创建一张模拟的InnoDB表
(2)插入一条记录表示sequence
因为DDL会默认进行一次隐式提交动作,所以逻辑2中的insert 记录也会一起提交掉。
虽然保证了持久化,但逻辑1和逻辑2中间有一个时间差,所以会有以下两种情况存在问题:
情况1:
如果session2在这个间隙发起select nextval查询,会查询不到记录;
情况2:
如果session2的隔离级别>=repeatable read. 同样看不到记录;
解法:
情况1是存在的,系统会给一个ERROR 1098 (42000): sequence未初始化的错误,重新发起一次就可以了。
情况2可以用下面提到的【5.摒弃MVCC】来解决,也就是sequence的读取,虽然是使用支持MVCC的InnoDB,但sequence value的查询实际上都是read uncommitted,即不需要多版本的。
3.
sequence查询的侵入
因为Sequence的查询没有增加和Oracle一致的select seq.nextval from dual;而是沿用了正常表的查询方式,所以,在handler接口层,对查询进行了侵入,如果缓存命中,就直接从InnoDB层返回,如果未命中,会从select语句转换层update语句,并进行了两阶段提交动作。
4.并发的控制
Sequence的查询,使用了每一个sequence 一个mutex的方法,维护缓存和记录的一致性。如果命中的话,mutex只是保护了currval的内存运算,并不会引起太大的并发问题。
5.摒弃MVCC
sequence的currval,nextval,是不维护多版本的,以此来保证sequence的特性,实现这个可以有两种方式:
方式1:
可以临时设置当前session,或者这个语句的隔离级别设置成read uncommited。
方式2:
在语句读取的时候,临时创建一个readview,读取最新的数据,因为有4.的并发控制方法,新的readview一定会读到最新的数据。
这里的移植,使用的是方式2,
因为方式2,还要解决[6. 自治事务的处理]
6.自治事务的处理
考虑当前的一个场景
update t1 set col1=1 where id=1;
select nextval from seq_1;
rollback;
也就是当当前事务回滚的时候, select nextval from seq_1不回滚,也不能回滚,否则破坏了sequence的特性。
那这里就需要一个自治事务,也就是select nextval from seq_1因为缓存不命中,而退化成update 语句的时候,这个update语句产生的事务必须和当前的事务脱离。
变成了下面的这种场景:
父事务
子事务, 并提交
回滚
子事务,是永远提交,并且不影响父事务的自治事务。
innoDB可以通过innobase_trx_allocate(user_thd);
临时分配一个transaction来实现。
7.主备复制
因为sequence的select nextval from seq语句如果缓存不命中,会退化成一个自治事务,包括了一个update seq语句,所以,自治事务需要记录binlog,来同步到备库,完成备库的sequence 更新。所以自治事务同样需要binlog和innoDB参与的内部XA事务,参考8.两阶段提交协议处理
8.两阶段提交协议处理
sequence所产生的XA事务和原本MySQL的内部XA事务并没有什么区别,同样需要两阶段提交协议进行处理,即
1. 分配一个全局XA id
2. binlog prepare
准备begin--table_map--update_row--commit
一个event group,包含了sequence的更新。
3. InnoDB prepare
持久化redo
4. binlog commit
持久化binlog
event
5. InnoDB commit
更改事务为commited状态,并flush
redo。
当MySQL crash的时候,sequence按照相同的崩溃恢复方式来完成recovery。
9.主备环境的切换
对于sequence的查询,主库和备库走了完全不同的路径,即:
如果是主库:
查询会通过sequence cache,并在cache用完的情况下,变成update语句。
如果是备库:
查询会跳过sequence cache,完全的只读。
10.缓存的失效处理
缓存失效,即sequence cache和InnoDB保存的记录不一致的情况下,就会产生这个问题,
比如一个场景:
如果当前的DB的角色切换 [主库-->备库-->主库],在从主库-->备库切换的时候,sequence cache就会失效,因为,备库会接受binlog event来更新InnoDB保存的记录,这样cache和db就会不一致。所以当又变成主库的时候,就会失去了sequence的order 特性。但注意,cache既然已经分配,虽然失效,但仍然会保持sequence的唯一性的特性。
所以,我们这里在set global readonly=[on/off]的时候,递增了一个global_readonly_version, 当sequence cache中保存的version和当前系统中的version不一致的情况下,需要重新load数据,这样就可以避免这个问题。但频繁设置readonly的时候,会导致没有使用完的cache被丢弃掉,但这个副作用,显然没有问题。
11.数据字典查询
Sequence作为一个新的对象类型,需要一些字典信息来查询,我们提供了两类查询
11.1 对象的查询:
可以通过select * from information_schema.tables where TABLE_TYPE= ’Sequence
table‘来查询所有的sequence。
11.2 参数的查询:
可以直接使用select * from seq,就可以查询这个seq的定义了。
但注意:这里因为有nextval,会推高nextval的值,如果不系统增加currval,就指定columns,
比如: select start, minvalue,maxvalue, increment from seq,
这样就不会推高currval。