悲观锁:总是假设最坏的情况,每次去拿数据的时候认为somebody会修改,所以每次在拿数据的时候都会上锁,传统的关系型数据库里就用到了很多的这种锁机制,如行锁,表锁,读锁和写锁等,都是在操作之前先上锁;Java中Synchronized和ReetranLock等独占锁就是悲观锁实现的。
悲观锁的实现方式:
悲观锁的实现是依赖于数据库提供的锁机制,流程如下:
1、修改记录前,对记录加上排它锁(exclusive locking)
2、如果加锁失败,说明这条数据正在被修改,那么当前查询要等待或者抛出异常,这由开发者决定
3、如果加锁成功,可以对这条数据修改了,事务完成解锁。
4、加锁修改期间,其他事务也想这条记录进行操作时,都要等待或直接抛出异常
在使用mysql innodb引擎实现悲观锁时,必须关闭mysql的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
eg:
-- 0.开始事务
begin;
-- 1.查询出商品库存信息
select quantity from items where id=1 for update;
-- 2.修改商品库存为2
update items set quantity=2 where id = 1;
-- 3.提交事务
commit;
使用select...for update会把数据锁住,MySQL InnoDB默认行级锁,行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
乐观锁:总是假设最好的situation,每次去拿数据时都believe别人不会修改,所以不会上锁,但是更新的时候会判断一下在此期间别人有没有去更新了这个数据(用版本号和CAS算法实现);乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write-condition机制,其实都是提供的乐观锁。
乐观锁的实现方式
乐观锁的实现不需要借助于数据库锁机制,只需要两个步骤,冲突检测和数据更新,其中一种典型的实现方法就是CAS(Compare and Swap)
CAS实现乐观锁
CAS是一种乐观锁实现方式,顾名思义就是先比较后更新。在对一个数据进行更新前,先持有这个数据原有值的备份。比如要将a=2更新为a=3,在进行更新前会比较此刻a是否为2,如果是2,才会进行更新操作,当多个线程尝试使用CAS同时更新一个变量时,只有一个线程能够成功,其余都是失败,失败的线程不会被挂起,而是被告知这次竞争失败,并且可以再次尝试。
-- 查询出商品库存信息,quantity = 3
select quantity from items where id=1
-- 修改商品库存为2
update items set quantity=2 where id=1 and quantity = 3;
在更新之前,先查询库存表中当前库存数,然后在做update时,以库存数作为一个修改条件。当进行提交更新的时候,判断数据库的当前库存数与第一次取出来的库存数进行比对,相等则更新,否则认为是过期数据,但是这种更新存在一个比较严重的问题,即ABA问题。
ABA问题
A线程取出库存数3,B线程取出库存数3,B线程先将库存数变为2,又将库存数变为3,A线程在进行更新操作时发现库存仍然是3,然后操作成功。尽管A线程操作时成功的,但是不能代表这个过程就是没问题的。
解决ABA问题的一个方法是通过一个顺序递增的version字段:
-- 查询出商品信息,version = 1
select version from items where id=1
-- 修改商品库存为2
update items set quantity=2,version = 2 where id=1 and version = 1;
在每次执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据版本号一致就可以执行修改操作并对版本号执行+1操作,否则执行失败,因为每次修改操作都会将版本号增加,所以不会出现ABA问题,还可以使用时间戳,因为时间戳自然具有顺序递增性。
乐观锁和悲观锁
乐观锁并不是真正的加锁,优点是效率高,缺点是更新失败的概率比较高;悲观锁依赖于数据库锁机制,更新失败的概率比较低,但是效率也低。