问题描述
系统数据越来越大,按照租户id来对数据分库,没有采用类似snow-flake这种全局id方式,而是用的auto_increment_offset+auto_increment_increment这种,各个库有自己的offset,步长各个库都是一样的,这样增长的id就保证了全局唯一性。线上采用的是前置数据库代理+一主多从,,由于increment和offset只能在数据库实例级别设置,不能在库级别设置,因此开发环境是在一个实例上建多个库+在connection初始化话的时候设置session的offset来模拟。在发到预生产测试的时候发现保存多条数据时第二条数据取到的id是第一条数据的id+1,并不是加步长,但是存储到数据库中的2个id却是按步长递增的。
问题分析
数据存储到数据库是按步长递增,说明数据库的步长设置起了作用,通过mybatis返回的id却没有按步长递增而是按1递增,那么问题应该出在mybatis身上。猜测可能是mybatis取到last_insert_id()后根据影响的条数循环做+1。
问题排查
既然怀疑是mybatis的问题,那就开始排查,
image.png
进去之后是调 stmt.getGeneratedKeys取得数据库生成的id,在pstmt每次执行完数据库后都会把生成的第一条数据id设置到rs的updateId,因此对updateCount进行循环加步长,
image.png
image.png
debug发现beginAt值是对的,那么猜测connection.getAutoIncrementIncrement()应该就是错的,而increment这个值是怎么来的呢? image.png
关于serverVariables的描述:
image.png
serverVariables在每次连接建立的时候就会去读取数据库的信息,也就是show variables的内容(当然只取一部分变量,具体参考jdbc驱动的ConnectionImpl.loadServerVariables方法)。
至此我们大概猜到问题的原因:预生产有数据库代理会解析sql,select请求走从库,insert,update,delete走主库。在取serverVariables的时候连的是从库,从库没有设置步长,默认是1,导致connection里面存储的步长是1,mybatis在计算generatedKeys的时候用的就是connection里保存的步长,因此导致计算出来的数据不对。
问题解决
数据库连接初始化的时候,就已经把数据库服务器变量保存到connection对象里了,而主从库的步长设置不一样,导致问题产生,把从库的步长设置和主库一样,问题解决,由于从库不会接受业务层面的数据更新请求,只是数据同步,在同步的时候id已经都有值了,因此不再走offset+步长那一套逻辑,从库设置步长不会产生副作用。
延伸思考
既然serverVariables是在连接建立的时候就已经读取过了,那么在刚建立成功时候如果执行初始语句改了session变量的话,那么connection里保存的值会自动刷新吗?
前面我说了,我们在开发环境用的是一个实例多个数据库,而offset和步长是设置在实例上的,为此每个数据库连接在建立的时候都执行了初始语句,阿里druid连接池支持初始化语句:
biz2.connectionInitSqls=set session auto_increment_offset=2
我们通过这种方式为每个datasource设置了session级别的offset(步长是global级别)。这里设置offset对我们代码没有什么影响。如果我们在connectionInitSqls里设置了session级别的步长,那么后面在取generatedKeys的时候,用的是数据库设置的global级别的步长,还是session级别的步长?
设置druid的connectionInitSqls,
set session auto_increment_offset=2;set session auto_increment_increment=200;
同时DruidDatasource开启initVariables,虽然在AbstractDruidDatasource的initPhysicalConnection方法里有去执行show variables,取到的结果保存在HashMap里,但是这个HashMap并没有在别的地方被引用到。测试插入多条数据,从connection里拿到的还是global级别的步长,connectionInitSqls设置的session级别步长没起作用。
网友评论