java并发之加锁

这段时间有一个需要是要“开宝箱”,涉及到一个事务中有多个数据库表要同步更新,初始阶段没有加锁导致一系列并发问题(当大脑沉浸在业务逻辑上的思考时候很容易忽略,原谅我的狡辩),期间遇到一些“灵异现象”,但最后在高手的指点下渐渐明白了一些以前留在大脑却没有理解深刻的概念,比如分布式锁、悲观锁、乐观锁等。下面按照我解决问题的先后顺序和问题发酵的流程,一些罗列遇到的各种坑。

ReentranLock

第一步就是想在代码中加入同步锁,结构先是如下:

这里写图片描述

在整个黑色框代表的事务(逻辑包括从数据库中获取宝箱中的物品,按照权重随机函数选择其中一个物品,然后更新该物品为已使用)中,一开始把同步锁加在1,即锁住整个事务,进行junit测试并发,并发问题马上解决。初始成功后,为了优化下性能,我把同步锁换成在2的位置,此时并发问题再次到来——并发10个线程开启宝箱,可是最后数据库里消耗的物品记录并不是10个,而是3-4个,查看日志有好几条物品记录被重复消费。此时我的思维凝固在1和2之间的区域的差集中努力寻找bug,可是这段代码除了查询还是查询,不可能会存在并发问题。经过一番思考后发现其实错误不在这里。2位置将并发的线程都挡在这里,只有一条线程进入,当第一条线程完成并释放锁后,还未来得及提交事务commit,第二个进入的线程就已经获得锁并执行完了代码,如此反复出现了并发问题。了解到原因后我又把同步锁切换到位置1,此时思考即便放在1,只要速度过快,还是有可能会有并发问题,而且生成的服务是分布式部署的,这样只能控制单台机器,多台机器下还是可能会选择到同一个物品,此时萌发的第二个想法是进行分布式锁。

分布式锁

简单的将上述的同步锁换成分布式锁,从逻辑上就不会再有并发问题了,但是同时又引入了一个问题,这个开宝箱的逻辑,在分布式环境下,同个时间只能由一台机器中的一个线程去执行,这除了发挥不了分布式服务的优势,还白白把多线程的给浪费了。从这里就可以感悟,分布式锁不适合这个场景,应该废弃。

mysql锁

上次的锁机制虽然能解决问题,但是性能大打折扣,应该选择粒度更小的锁,于是把锁机制从代码转移到了数据库。mysql的锁分为乐观锁和悲观锁。这两个概念相信很多人听过,但是真正应用的可能只有部分。

悲观锁

这个名字取得好,悲观锁就是在获取数据的时候(select)永远认为这些选择的记录都会被其他线程所修改,所以select 的时候总会加入排它锁,其他线程要想获取更新这些记录就要阻塞等待。在mysql中select记录最后最加for update,就可以使用排它锁将其锁住。
mysql的排它锁是利用索引键字段来控制的,如果select语句中where id=1,即明确主键,那么将会只锁住这一条记录。如果是select * from tb where age=10 limit 1;,其中age是索引,但不是唯一索引,mysql 会将所有age=1的记录都锁住。如果where中的条件不是任何索引,将会进行全表锁。所以一般要进行for update 的语句where筛选属性一般要加锁。

乐观锁

乐观锁认为大部分情况下获取数据是不会有并发问题,等到有问题在进行解决。所以乐观锁的select并不会加上for update。而一般在乐观锁的表中有一个version字段用于表明当前数据记录的版本号,我们在select 的时候已经获取出来version=1,而当要进行update的时候在where条件中加入version=1,如果有数据返回,证明记录没有被修改可以更新,如果获取不到,则本次更新失败,可以进行重试机制。乐观锁用与读多写少的场景,而乐观锁则相反,乐观锁广泛应用在nosql中

另外关于mysql在innodb存储引擎下关于查询的条件是否使用行锁还是表锁,请参考这边文章
http://blog.csdn.net/claram/article/details/54023216

猜你喜欢

转载自blog.csdn.net/jerryJavaCoding/article/details/78848336