这几天和同事一直在讨论关于表设计中主键选择的问题,用sequence作为主键究竟有什么好处,又有什么缺点,尤其是有些事务场景上下文需要用到创建的序列值,如何用?其实我想说的是,可能只是一个很简单的概念,可能深入理解,还是有很多未知的知识,当然也就可能会有一些容易忽略但又可能很关键的坑,只有碰了才知道。。。
以下是总结摘要,如有疏漏,还请过路的各位大侠赐教。
1、首先说下seq.nextval主要有以下两种使用场景:
(1). 如果一个事务中只是INSERT时需要序列,其他地方不会需要这个序列,那么只需要在INSERT ... VALUES (seq.nextval ...)语句中使用即可。
(2). 如果一个事务中INSERT一张表后,还需要插入时的主键ID值,作为外键插入其他表,那么就需要在INSERT第一张表前使用select seq.nextval from dual提前获取可用的ID保存到一个变量中,为后面使用。
(1). 如果一个事务中只是INSERT时需要序列,其他地方不会需要这个序列,那么只需要在INSERT ... VALUES (seq.nextval ...)语句中使用即可。
(2). 如果一个事务中INSERT一张表后,还需要插入时的主键ID值,作为外键插入其他表,那么就需要在INSERT第一张表前使用select seq.nextval from dual提前获取可用的ID保存到一个变量中,为后面使用。
2、其次可以简单说下调用序列的原理,只有理解了序列的原理,才能有助于我们知道如何正确使用序列。
使用序列时Oracle内部大体是按照如下步骤进行:
(1). 一个序列会被定义到Oracle内部的一张数据字典表(seq$)的一行。
(2). 第一次使用序列,序列的起始值会加上缓存大小,然后更新回行。
(3). Oracle内部会自动跟踪内存中的两个值,当前值和目标值。
(4). 每次有回话调用seq.nextval,Oracle会递增当前值,然后检查是否超过了目标值,再返回结果。
(5). 如果当前值和目标值相同,Oracle会更新数据字典表中的行,为目标值加上缓存大小,同时内存中产生了一个新的目标值。
例如create sequence seq cache 20;这样一序列。名称为seq的序列,缓存大小是20,默认初始值是1,步长默认是1。
当使用了一次seq.nextval后,可以看HIGHWATER字段值为21,即目标值1+缓存大小20=21。
当执行20次后,seq.nextval值变为21,此时HIGHWATER字段值是41,即目标值21+缓存大小20=41。
也就是每调用seq.nextval值20次,会更新一次seq$表,那么问题来了,如果cache值较小,且序列使用的频率较高,那么会对seq$表有频繁的更新操作,日志量会增加,尤其在RAC下,更新该行的时候,该数据块会在节点间不停的传送,就会产生可能的争用,这种问题会被放大。因此为了减少这种情况,我们可以将cache缓存值设置大一些,例如1000,减少对字典表的更新。
序列还有一个问题,就是cache缓存是实例级的,对于RAC,比如第一个节点使用序列时会分配1-20,第二个节点会被分配21-40,Oracle保证不会重复,但若节点crash了,比如节点1坏了,那么序列就会出现断号,节点1再次使用时,只会从41-60,由于我们用主键只为了标示唯一,不关心段号,也不关心产生的顺序,所以这些可以忽略。
注:最近在讨论某系统和一个外系统做全局事务的事情,本想用这个主键作为两系统传输的一部分,用于控制全局事务,且用其作为判断交易先后顺序的依据,这是不太符合要求的,因为是RAC,序列是基于实例级cache,那么如果不能保证某一类型的交易总在一个节点上执行,那么不同次交易产生的主键序列值可能不是递增的,例如节点1处理一次交易,产生序列是1,节点2处理一次交易,产生序列是21,此时节点1再处理一次交易,产生序列是2;除非设置序列为order,但这样在RAC就有可能产生资源争用的问题,因为为了保证多节点间每次产生的序列值是递增的,每次产生就需要多节点间判断当前值后,才能知道下一个值是多少,而且会有额外的锁,保证同一时间只有一个节点在做这个操作,当然究竟是否会产生资源争用,还是要依据实际的业务并发量,或者压力测试才能证明,这里只是说可能会这样的问题,不是一定会,否则Oracle就不会有提供这种order的创建属性,凡事不绝对。
3、结合(1)的场景,
(1). 如果一个事务中只是INSERT时需要序列,其他地方不会需要这个序列,那么只需要在INSERT ... VALUES (seq.nextval ...)语句中使用即可。
> 这个场景下,如果序列cache设置为1000,调用100次nextval几乎没有影响。
(2). 如果一个事务中INSERT一张表后,还需要插入时的主键ID值,作为外键插入其他表,那么就需要在INSERT第一张表前使用select seq.nextval from dual提前获取可用的ID保存到一个变量中,为后面使用。
> 这个场景,就有些说的了。如果是同一事务中需要用到之前的序列值,那么就需要提前用select seq.nextval from dual保存到变量中,(当然,如果是用PLSQL语句,则可以不用提前保存变量这步,但咱们的应用中不适用,就不多说了)。
> 这个场景,就有些说的了。如果是同一事务中需要用到之前的序列值,那么就需要提前用select seq.nextval from dual保存到变量中,(当然,如果是用PLSQL语句,则可以不用提前保存变量这步,但咱们的应用中不适用,就不多说了)。