悲观锁和乐观锁
悲观锁(Pessimistic Lock)
悲观锁是基于数据库层面的锁, 就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block阻塞。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,写锁等,都是在做操作之前先上锁。
一个典型的倚赖数据库的悲观锁调用:select * from style where id='1' for update 这条 sql 语句锁定了style表中所有符合检索条件(id='1')的记录。这条数据就被当前事务锁定了,其它的事务必须等本次事务提交之后才能执行,也就是其他线程进入阻塞状态。这样可以保证当前的数据不会被其它事务修改。
ps:id字段一定是主键或者唯一索引,不然是锁表,会出事的。
本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。如果另外的事务中如果再次执行select * from style where id='1' for update;则这第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,如果在第二个事务中执行select * from style where id='1';则能正常查询出数据(不能进行更新操作),不会受第一个事务的影响。
例如: 事务一:
# 开启事务 START TRANSACTION; select * from style where id=1 for update; UPDATE style SET style_name= '555' WHERE id = 1; #COMMIT;不提交事务
事务二:
select * from style where id=1 for update;
可以看到正在处理,一直处于阻塞状态,直到超时查询失败如下:
select * from style where id=1 for update 1205 - Lock wait timeout exceeded; try restarting transaction 时间: 51.009s
事务三:进行更新操作
update style set style_name='99999' where id=1;
可以看到正在处理,一直处于阻塞状态,直到更新失败如下:
update style set style_name='99999' where id=1 1205 - Lock wait timeout exceeded; try restarting transaction 时间: 51.009s
补充:MySQL select…for update的Row Lock与Table Lock
上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表给锁住)。
悲观锁机制存在以下问题:
①在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
②一个线程持有锁会导致其它所有需要此锁的线程挂起。
③如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
乐观锁(Optimistic Lock)
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号(version)机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
一般是通过为数据库表增加一个 "version" 或"timestamp"字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果此时的版本号等于数据库表的版本号,则予以更新,否则认为是过期数据(其他事务已经处理)。
update status set name='liar',version=(version+1) where id=1 and version=version;
假设数据库中帐户信息表中有一个version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
2 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
3 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时发现数据库已经不存在这条数据,因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。