最近在迁移代码,迁移过程中发现了一段应用了乐观锁的代码(这是伪代码):
public boolean doWork(final int count, Entity entity) throws InterruptedException { if (3 == count) { return false; } int j = dao.update(entity); if (j == 0) { Thread.sleep(100); count++; Entity entity = dao.selectEntity(entity.id); return doWork(count, entity); } return true; }
大概意思是先执行更新。更新对应的表有一个version字段,entity的实例中是包含当前版本号的,更新时版本号匹配则返回true;如果不匹配则去数据库查询新的版本号,如果重试达到三次则报错。其实递归次数不多本来这也没什么问题,但是问题在于这段代码在一个事务中,而事务用的是默认的隔离级别,关键是数据库用的是mysql,和大多数数据库有点不一样,它用的默认隔离级别是可重复读(REPEATABLE_READ)。
于是问题出现了,可重复读的隔离级别中,除非更新操作执行成功,否则同一个事务对同一条记录的读取返回总是一样的,也就是说,这个版本号如果出现不匹配,那后面三次重试一定不会成功,因为重新读出来的版本号就不是最新的,根本就没变:
具体的测试代码在:https://github.com/saaavsaaa/warn-report/blob/master/src/test/java/DBTest.java。
也就是说这个乐观锁白写了。。。
其实乐观锁主要是应对并发问题,减少数据库的排队的手段,其实更多是应用在抛开事务的场景下来保证执行正确的。不过说并发其实也抗不了多少并发,就算不用事务,这种使用数据库去抗所有处理的方式。。。,其实我更喜欢用Redis解决类似情况,因为单线程、因为快、因为减少持久化存储的压力,数据库其实是互联网应用中相对最经不起故障的重要部分。将计数保存在Redis中,所有操作都只对Redis的值进行操作,发现Redis中无值,通知同步服务同步数据到Redis中,然后再计算服务从Redis取值计算,不过要注意同步服务需要判断Redis有值了,就不执行写操作了。
另外,降低隔离级别也可以解决这个用例的问题(其实读已提交级别是其他大多数数据库的默认隔离级别),下面方法仅供参考,上生产出问题不负责。。。,下面是读未提交的测试中默认5.6版本的库需要改的配置:http://www.cnblogs.com/saaav/p/5943393.html,spring当中有七种事务传播规则:PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。(两个事务互补干扰 各自管理各自的事务),可以对乐观锁方法使用这个配置,其他事务使用默认级别,降低风险,虽然我也不知道有什么风险,目前也没时间深入研究,但是数据库开发团队既然决定使用这个默认级别,应该是有什么原因。。。
那个测试类里还有一个测试并发的方法testConcurrentUpdate,我机器配置一般,更新并发超过1100就有失败的了,1500的时候失败已经过半了,大部分是锁超时,不过并发写到这个程度一般也不会用数据库生抗了,所以细节就不说了,知道有这么个事就成了。
==========================================================
咱最近用的github:https://github.com/saaavsaaa
微信公众号:
转载请注明出处