最近在新东家做审批系统,业务逻辑和工作流引擎都严重依赖mysql。其中业务逻辑部分大量用到存储过程,几乎所有核心dal都是用存储过程实现的。使用存储过程的优势是对于复杂的数据插入和更新操作效率很高。如果不使用存储过程,一个复杂的操作可能要请求多次数据库才能完成,但使用存储过程只需要请求一次,节省网络请求开销。但存储过程的坏处也很明细:不方便测试和调试。我们前一阵就碰到一个存储过程的bug,困扰了一阵。
有一天,同事发现数据库里面存的某个字段和原始申请单里面的字段不一样。而且奇怪的是,并非所有的申请单都是这样,有的是对的,有的不对。我检查了所有可能插入和修改该字段的代码,发现总共只有两个地方会写入该字段,并且这两个地方都是新增记录,不会修改。按理说只要存储过程参数传递正确,该字段就会正确地写入,并且不再变化。但事实却是,这两个地方写入的两种记录都会不确定性地出现字段被篡改的问题。
凭直觉,这种不确定性出现的问题,有可能是线程安全问题引起的。一开始,我们首先怀疑是参数在某个地方被非线程安全的访问,导致被诡异修改。排查代码,我们发现代码逻辑没有问题,不存在这种可能性。那只可能是存储过程的问题,后来终于找到原因。
这个bug是这样产生的。我们数据库里有一些码表,用来定义一些业务相关的类型,比如商品类型表(goods_type),它的定义类似这种:
|id|code|description| |--|----|-----------| |-2| XER| code有误 | |0 | A | 日用品 | |1 | B | 家电 | |2 | C | 厨房电器 |
用户的申请单中会包含商品类型信息(商品类型code),申请单保存到数据库的时候,会为每件商品存储一条记录,记录中包含该商品在商品类型表中的id。
用于保存商品信息部分的存储过程中有类似这样的语句:
select id into @goods_id from good_type where code=@goods_code;
if @goods_id = NULL then
set @goods_id = -2;
end if
上面语句中@goods_id
,@goods_code
都是会话变量,会在同一个会话中共享。@goods_code
是从存储过程参数中得到的商品code。
当@goods_code
是一个在goods_type中找不到的code时,上面的select
语句不会修改@goods_id
的值,因此@goods_id
的值会是上一次修改过后的值。如果当前会话之前的语句恰好修改过@goods_id
,那该商品保存的商品类型id就会“莫名其妙“的被篡改。如果当前会话之前的语句没设置过@goods_id
变量,那@goods_id
值为NULL,就会执行后面的if语句。由于我们使用数据库连接池来访问数据库,连接会被复用,同一个会话就执行多条不同的语句,所以就会不确定性的出现字段被篡改的现象。
有两种方式修复该bug,一是在select
语句前加上初始化语句set @goods_id = NULL;
,这样每次使用前都保证会重新初始化变量;另一种方式是改成使用局部变量,不用会话变量。
前一阵我们发现mysql写入、删除压力大,于是决定对其中一个关键表分区,缓解压力。该表有varchar(64)类型的主键,并且主键的生成策略类似UUID。我们使用mysql的key函数按主键分区,一开始设置分区数为64,发现所有的数据全分到一个区。刚开始怀疑是分区语句的问题,但经过确认,分区语句没啥问题。后来调整分区数,发现分区数为奇数时分区比较均匀,偶数有问题。
‘c7e74234-40c7-11e6-a64b-7ce9d3efdb89‘,
‘c7e74232-40c7-11e6-a64b-7ce9d3efdb89‘,
‘c7e74230-40c7-11e6-a64b-7ce9d3efdb89‘,
‘c7e609a5-40c7-11e6-a64b-7ce9d3efdb89‘,
‘c7e609a3-40c7-11e6-a64b-7ce9d3efdb89‘
观察生成的主键,发现主键中间和后面的部分都一样,只有前面部分不同,猜测key函数分区实现和后面部分关系比较大。有空看看源码分析一下具体原因。
原文:https://www.cnblogs.com/segeon/p/10381127.html