一。定义:
乐观锁:就是在操作时很乐观,这数据只有我在用,我先尽管用,最后发现别人修改了数据则回滚。(应用层加锁)
悲观锁:在操作时很悲观,认为其他人同时更新,因此我就先将其先锁住,让别人用不了,我操作完成后再释放掉。(数据库层加锁 for update)
二。实现方式
悲观锁:法1.对代码块加锁(如Java的synchronized关键字);法2:对数据加锁(如MySQL中的排它锁 for update,其他的事务是可以读取的。但是不能写入或者更新。
顺带一提的是,当选中某一个行的时候,如果是通过主键id选中的。那么这个时候是行级锁。
其他的行还是可以直接insert 或者update的。如果是通过其他的方式选中行,或者选中的条件不明确包含主键。这个时候会锁表。其他的事务对该表的任意一行记录都无法进行插入或者更新操作。只能读取)。
乐观锁:法1:版本号机制;法2:CAS机制
三。优缺点
1.功能限制
2.并发冲突的概率
出现并发冲突的概率小时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
(乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了)
注意:乐观锁不能解决脏读的问题。
出现并发冲突的概率大时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
四。举例
考虑这样一种场景:游戏系统需要更新玩家的金币数,更新后的金币数依赖于当前状态(如金币数、等级等),因此更新前需要先查询玩家当前状态。
下面的实现方式,没有进行任何线程安全方面的保护。如果有其他线程在query和update之间更新了玩家的信息,会导致玩家金币数的不准确。
@Transactional //不加锁 public void updateCoins(Integer playerId){ //根据player_id查询玩家信息 Player player = query("select coins, level from player where player_id = {0}", playerId); //根据玩家当前信息及其他信息,计算新的金币数 Long newCoins = ……; //更新金币数 update("update player set coins = {0} where player_id = {1}", newCoins, playerId); }
为了避免这个问题,悲观锁通过加锁解决这个问题,代码如下所示。在查询玩家信息时,使用select …… for update进行查询;该查询语句会为该玩家数据加上排它锁,直到事务提交或回滚时才会释放排它锁;在此期间,如果其他线程试图更新该玩家信息或者执行select for update,会被阻塞。
@Transactional
public void updateCoins(Integer playerId){
//根据player_id查询玩家信息(加排它锁)
Player player = queryForUpdate("select coins, level from player where player_id = {0} for update", playerId);//悲观锁 排他锁
//根据玩家当前信息及其他信息,计算新的金币数
Long newCoins = ……;
//更新金币数
update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}
乐观锁:版本号机制则是另一种思路,它为玩家信息增加一个字段:version。在初次查询玩家信息时,同时查询出version信息;在执行update操作时,校验version是否发生了变化,如果version变化,则不进行更新。
@Transactional public void updateCoins(Integer playerId){ //根据player_id查询玩家信息,包含version信息 Player player = query("select coins, level, version from player where player_id = {0}", playerId); //根据玩家当前信息及其他信息,计算新的金币数 Long newCoins = ……; //更新金币数,条件中增加对version的校验 update("update player set coins = {0} where player_id = {1} and version = {2}", newCoins, playerId, player.version);//乐观锁 版本号机制 }