乐观锁与悲观锁

记得曾经面试的时候,面试官问到过这个问题,但是问题是关于Hibernate的,时隔快1年了,整理了下。

前言

在数据库的锁机制中,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。

针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。

下面分别讨论一下悲观锁和乐观锁。

悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

悲观锁(Pessimistic Locking),顾名思义,对每次数据被外界的(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持悲观态度。每次外界在操作数据时,对数据进行锁定,直到当前操作完成以后释放锁,允许其他操作。在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁:

1
2
3
INSERTUPDATEDELETESELECTFOR UPDATE [OF columns] [WAIT n | NOWAIT];
SELECTFOR UPDATE语句允许用户一次锁定多条记录进行更新
使用COMMITROLLBACK语句释放锁

Example: MySQL InnoDB中使用悲观锁

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

1
set autocommit=0;  取消自动提交,当autocommit=1时候为立即提交

1.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
2.查询出商品信息

1
select status from t_goods where id=1 for update;

3.根据商品信息生成订单

1
insert into t_orders (id,goods_id) values (null,1);

4.修改商品status为2

1
update t_goods set status=2;

5.提交事务

1
commit;/commit work;

表级锁又分为5类:

  • 行共享 (ROW SHARE) – 禁止排他锁定表
  • 行排他(ROW EXCLUSIVE) – 禁止使用排他锁和共享锁
  • 共享锁(SHARE) - 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁
  • 共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止使用共享锁及更高的锁
  • 排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表。

优点与不足

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
但在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;
还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

乐观锁

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。

它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

乐观事务控制最早是由孔祥重(H.T.Kung)教授提出。

乐观锁(optimistic locking),顾名思义,每次去拿数据的时候都认为别人不会修改,允许多个人同时修改同一文件。在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

使用版本号实现乐观锁

使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号。

1.查询出商品信息

1
select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单

3.修改商品status为2

1
2
3
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

优点与不足

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

两种锁各有优缺点,需要根据具体的场景进行响应的选择。
乐观锁适用于常读少写的情况下,即冲突很少发生的时候,可以省去了锁的开销,减少数据库开销,加大系统的吞吐量。
悲观锁则适用于常写少读的情况,确保操作的原子性。减少数据的冲突,保证数据的准确性。

参考:
1.数据库锁机制
2.深入理解乐观锁于悲观锁
3.一分钟教你知道乐观锁和悲观锁的区别