首页IT科技序列编码(序列号生成并发引发的synchronized、数据库隔离级别、myabits缓存等一些问题记录)

序列编码(序列号生成并发引发的synchronized、数据库隔离级别、myabits缓存等一些问题记录)

时间2025-07-29 19:05:18分类IT科技浏览4693
导读:起因 一个序列号产生方法发现有并发问题。 修改这个方法中发生了一些错误,而这涉及到了一些的知识点,所以记录下。...

起因

一个序列号产生方法发现有并发问题               。 修改这个方法中发生了一些错误              ,而这涉及到了一些的知识点                      ,所以记录下                     。

涉及点

synchronized方法:如果此方法内包含数据库操作       ,且外围有事务时       ,并不能完全锁住       。 数据库隔离级别 RC-不可重复读                      ,也就是在同一个事务中               ,多次select同一条sql获取的结果可能不同               。 RR-可重复度       ,同一个事务中                     ,多次select同一条sql获取的结果相同               ,除非中间update进行了数据修改                      。 mysql的默认隔离级别是RR,其它的数据库一般默认级别是RC       。 mybatis缓存                     ,在有事务的情况下                      ,多次select同一个sql,第二次将不再请求数据库              ,而是直接从缓存读取       。

描述

情景一

类似的代码如下                      ,下发的代码在并发时就会出现重复的code:

public Integer getCode(){ CodeRule codeRuleInfo = selectCodeRuleInfo();//1               、从数据库获取当前的记录 Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1 codeRuleInfo.setCode(newCode);//2                     、赋值最新的 updateCodeRule(codeRuleInfo);//3       、更新数据库 return newCode; }

由于是单机环境       ,最开始就想用synchronized解决              ,但发现getCode本身是在一个事务下的                      ,这样就导致仅仅靠synchronized并不能锁住       ,原因如下:

1-事务开始 -> 2-synchronized开始 -> 3-synchronized结束 -> 4-事务提交

其中3和4之间会又时间差       ,导致这期间如果有并发进来                      ,还是会出现重复               ,所以需要改成:

1-synchronized开始 -> 2-事务开始 -> 3-事务提交 -> 4-synchronized结束

正常的改进应该是:

//原有方法       ,增加synchronized锁 public synchronized Integer getCode(){ return AAA.getCode(); } //另一个类中AAA                     ,开启新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public Integer getCode(){ CodeRule codeRuleInfo = selectCodeRuleInfo(); Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1 codeRuleInfo.setCode(newCode); updateCodeRule(codeRuleInfo); return newCode; } 情景二

当时改的时候脑子抽了               ,并没有按照上方的改,而是改成这样了:

//原有方法                     ,增加synchronized锁 public synchronized Integer getCode(){ CodeRule codeRuleInfo = selectCodeRuleInfo(); Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1 codeRuleInfo.setCode(newCode); AAA.updateCodeRule(codeRuleInfo);//只把更新放到了新事务中 return newCode; } //另一个类中AAA                      ,开启新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public Integer updateCodeRule(){ //执行sql更新 }

上述方法就导致一个问题,select方法在主事务中              ,而update方法在新事务中                      ,部署到线上果然不但没解决       ,反而非并发仅仅是循环中就全部重复了              ,原因如下:

mysql默认的隔离级别是RR                      ,也就是同一个事务内多次select同一个语句       ,获取的结果都是一样的       ,除非两个select之间有update操作才会重新获取最新数据                      。但我们的update方法放在了新的事务中                      ,导致并没有对select产生影响               ,所以循环下的getCode就会产生重复数据              。

但是诡异的是       ,我们在部署前有作过简单的测试                     ,测试是正常递增的               ,找了半天才发现是mybatis缓存的原因:

mybatis一级缓存会对多次相同的sql查询进行缓存,第二次就不会再读取数据库了                     ,而是直接从缓存中获取                      ,除非中间进行了数据库的增删改,而新事务中增删改同样不影响缓存清除              ,听起来和数据库的RR隔离级别很像                      ,但这里有个本质区别       ,它缓存的是一个对象              ,如果你对这个对象中的数据进行了修改                      ,那么下次获取的是修改后的数据       ,而我们代码中每次都有codeRuleInfo.setCode(newCode);来对此对象进行了修改       ,所以在mybatis缓存的作用下                      ,取出的数据却是正常递增的       。

这样就又不对了               ,照理来说如果按照mybatis缓存       ,那正式环境也不应该会重复                     ,再进一步的排查               ,原因如下:

我们测试环境中的很简单,就一个select和一个update                     ,但我们正式环境中getCode仅仅是一个主事务中的一个小方法                      ,在循环中getCode后面还有一些其它表的增删改操作,而这些增删改会清空mybatis的缓存                      。 这里我们有一个误区              ,原以为mybatis缓存也是仅仅只有针对该表的增删改才会清除缓存                      ,但实际上是针对任意表的增删改都会清除缓存       ,所以我们线上环境是每次都会清缓存从数据库读取              ,然后就是数据库RR隔离级别导致数据重复                      ,而本地测试则是走缓存       ,正常增加              。

更好的并发处理

这种涉及到数据库又存在事务的       ,采用synchronized方法还是有些缺陷的:子事务独立于主事务                      ,如果子事务出问题               ,则不能整体回滚了。感觉更应该考虑用数据库的悲观锁和乐观锁来解决       ,或者是参考微服务的一些锁实现                      。 悲观锁实现比较简单点                     ,在并发不高时可以考虑               ,在select 语句中加上for update,但需要注意的是在RR隔离环境下                     ,如果select没有命中数据                      ,可能会产生间隙锁(很复杂,不同主键               、索引                      、非索引造成的锁类型各不相同              ,可自行搜索相关资料)                      ,此时又insert就可能会造成死锁                     。解决方法是设置数据库隔离级别为RC       ,因为RC模式下不会有间隙锁产生              ,从而不会死锁。 mysql数据库设置成RC隔离级别也没什么问题                      ,毕竟Oracle       、PostgreSQL等一大众数据库的默认级别就是RC       ,而阿里云等一些云厂商的RDS数据库隔离级别默认也是RC               。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
友情链接作用大不大(友情链接检测工具能有哪些功能,友情链接检测工具的使用说明) web自动化测试是什么(web自动化测试进阶篇02 ——— BDD与TDD的研究实践)