MySql悲观锁(⾏锁)和乐观锁
⼀、悲观锁VS乐观锁
引⾔
为什么需要锁(并发控制)
在并发的环境中,会存在多个⽤户同时更新同⼀条数据,这时就会产⽣冲突。
冲突结果:
丢失更新:⼀个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。
脏读:当⼀个事务读取其它完成⼀半事务的记录时,就会发⽣脏读取。
因此为了解决上述问题,引⼊了并发控制机制。
乐观锁(乐观并发控制)和悲观锁(悲观并发控制)是并发控制的主要⼿段, 其实不仅关系型数据库中有乐观锁和悲观锁的概念,像
redis,memcached等都有类似的概念。所以,不要把乐观锁和悲观锁狭隘的理解为DBMS中的概念,更不能把他们和数据库中提供的锁机制混为⼀谈。其实,在DBMS中悲观锁正是利⽤了数据库提供的锁机制实现的。
因此我们针对不同的业务场景,应该选择不同的并发控制⽅式。
⼀、悲观锁
悲观锁指的是外界对数据修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也⽆法保证外部系统不会修改数据)。
DBMS悲观锁是阻⽌⼀个事务以影响其他⽤户的⽅式来修改数据。如果⼀个事务执⾏的操作 某⾏数据应⽤了锁,那只有当这个事务把锁释放,其他事务才能够执⾏与该锁冲突的操作。
悲观并发控制主要⽤于数据争⽤激烈的环境,以及发⽣并发冲突时使⽤锁保护数据的成本要低于回滚事务的成本的环境中。
数据库中,悲观锁的流程:
对任意记录进⾏修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应⽅式由开发者根据实际需要决定。 如果成功加锁,那么就可以对记录做修改,事务完成后就会释放锁了。 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
使⽤悲观锁
要使⽤悲观锁,我们必须关闭mysql数据库的⾃动提交属性,因为MySQL默认使⽤auto commit模式,也就是说,当你执⾏⼀个更新操作后,MySQL会⽴刻将结果进⾏提交。set autocommit=0;
//0.开始事务
begin;/begin work;/start transaction; (三者选⼀就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息⽣成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;
上⾯的查询语句中,我们使⽤了select…for update的⽅式, 这样就通过开启排他锁的⽅式实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执⾏。这样我们可以保证当前的数据不会被其它事务修改。
注意点
我们需要注意⼀些锁的级别,MySQL InnoDB默认⾏级锁。 ⾏级锁都是基于索引的,如果⼀条SQL语句⽤不到索引是不会使⽤⾏级锁的,会使⽤表级锁把整张表锁住。上⾯我们使⽤select…for update会把数据给锁住(id上有主键索引),因此不会使⽤表级锁。
如果查询语句中有表关联,只是⽤for update会锁住所有表的相关记录,建议使⽤for update of t.id。如果你不想让你的事务等待其他事务的锁,可以在for update 后加上 nowait,这样当遇上冲突时数据库会抛出异常。
优势劣势
悲观并发控制实际上是"先取锁再访问"的保守策略,为数据处理的安全提供了保证。但是在效率⽅⾯,处理加锁的机制会让数据库产⽣额外的开销,还有增加产⽣死锁的机会;另外,在只读型事务处理中由于不会产⽣冲突,也没必要使⽤锁,这样做只能增加系统负载;还有会降低了并⾏性,⼀个事务如果锁定了某⾏数据,其他事务就必须等待该事务处理完才可以处理那⾏数。
⼆、乐观锁
相对悲观锁⽽⾔,乐观锁假设认为数据⼀般情况下不会造成冲突,所以在数据进⾏提交更新的时候,才会正式对数据的冲突与否进⾏检测,如果发现冲突了,则让返回⽤户错误的信息,让⽤户决定如何去做。
相对于悲观锁,在对数据库进⾏处理的时候,乐观锁并不会使⽤数据库提供的锁机制。⼀般的实现乐观锁的⽅式就是记录数据版本。
实现数据版本有两种⽅式:
第⼀种是使⽤版本号version,
update是什么第⼆种是使⽤时间戳timestamp(时间戳精度)。
数据版本,为数据增加的⼀个版本标识。当读取数据时,将版本标识的值⼀同读出,数据每更新⼀次,同时对版本标识进⾏更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第⼀次取出来的版本标识进⾏⽐对,如果数据库表当前版本号与第⼀次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
DBMS会假设多⽤户并发的事务在处理时不会彼此互相影响,各事务能够在不产⽣锁的情况下处理各⾃影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务⼜修改了该数据(修改该数据的事务已提交,或未提交;事务未提交会带来数据丢失)。如果其他事务有更新的话,正在提交的事务会进⾏回滚。
注:对于以上两种⽅式,Hibernate⾃带实现⽅式:在使⽤乐观锁的字段前加annotation: @Version, Hib
ernate在更新时⾃动校验该字段。使⽤版本号实现乐观锁
使⽤版本号时,可以在数据初始化时指定⼀个版本号,每次对数据的更新操作都对版本号执⾏+1操作。并判断当前版本号是不是该数据的最新的版本号。
//1.查询出商品信息
select status,version from t_goods where id=#{id}
//2.根据商品信息⽣成订单
//3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
注意第⼆个事务执⾏update时,第⼀个事务已经提交了,所以第⼆个事务能够读取到第⼀个事务修改的version。
下⾯这种极端的情况:
我们知道MySQL数据库引擎InnoDB,事务的隔离级别是Repeatable Read,因此是不会出现脏读、不可重复读。
在这种极端情况下,第⼆个事务的update由于不能读取第⼀个事务未提交的数据(第⼀个事务已经对这⼀条数据加了排他锁,第⼆个事务需要等待获取锁),第⼆个事务获取了排他锁后,会发现version已经发⽣了改变从⽽提交失败。
优势劣势
乐观并发控制相信事务之间的数据竞争(datarace)的概率是⽐较⼩的, 因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产⽣任何锁和死锁。
乐观锁虽然没有依赖数据库提供的锁机制,也可以保证数据⼀致性。
-------------------------------------------------------------------------------------------------------------------------------
⼆、什么是乐观锁,什么是悲观锁?
⼀、并发控制
当程序中可能出现的情况时,就需要保证在并发情况下数据的准确性,以此确保当前⽤户和其他⽤户⼀起操作时,所得到的结果和他单独操作时的结果是⼀样的。这就叫做并发控制。并发控制的⽬的是保证⼀个⽤户的⼯作不会对另⼀个⽤户的⼯作产⽣不合理的影响。
没有做好并发控制,就可能导致等问题。
常说的并发控制,⼀般都和数据库管理系统(DBMS)有关。在 DBMS 中并发控制的任务,是确保多个事务同时增删改查同⼀数据时,不破坏事务的隔离性、⼀致性和数据库的统⼀性。
实现并发控制的主要⼿段分为乐观并发控制和悲观并发控制两种。
⽆论是悲观锁还是乐观锁,都是⼈们定义出来的概念,可以认为是⼀种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像 hibernate、、memcache 等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进⾏对⽐。乐观锁⽐较适⽤于读多写少的情况(多读场景),悲观锁⽐较适⽤于写多读少的情况(多写场景)。
⼆、悲观锁(Pessimistic Lock)
1 理解
当要对数据库中的⼀条数据进⾏修改的时候,为了避免同时被其他⼈修改,最好的办法就是直接对该数据进⾏加锁以防⽌并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的⽅式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,⼜
名“悲观锁”】。
悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来⾃外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。。
之所以叫做悲观锁,是因为这是⼀种对数据的修改持有悲观态度的并发控制⽅式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进⾏加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:
1. 传统的关系型数据库使⽤这种锁机制,⽐如⾏锁、表锁、读锁、写锁等,都是在操作之前先上锁。
2. Java ⾥⾯的同步 关键字的实现。
2 悲观锁主要分为:
共享锁【shared locks】⼜称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同⼀数据可以共享⼀把锁,都能访问到数据,但是只能读不能修改。
排他锁【exclusive locks】⼜称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果⼀个事务获取了⼀个数据⾏的排他锁,其他事务就不能再获取该⾏的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据⾏读取和修改。
3 说明
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率⽅⾯,处理加锁的机制会让数据库产⽣额外的开销,还有增加产⽣死锁的机会。另外还会降低并⾏性,⼀个事务如果锁定了某⾏数据,其他事务就必须等待该事务处理完才可以处理那⾏数据。
三、乐观锁(Optimistic Locking)
1 理解
乐观锁是相对悲观锁⽽⾔的,乐观锁假设数据⼀般情况不会造成冲突,所以在数据进⾏提交更新的时候,才会正式对数据的冲突与否进⾏检测,如果冲突,则返回给⽤户异常信息,让⽤户决定如何去做。乐观锁适⽤于读多写少的场景,这样可以提⾼程序的吞吐量。
乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的⼀种机制,但乐观锁不会刻意使⽤数据库本⾝的锁机制,⽽是依据数据本⾝来保证数据的正确性。乐观锁的实现:
1. CAS 实现:Java 中urrent.atomic包下⾯的原⼦变量使⽤了乐观锁的⼀种 CAS 实现⽅式。
2. 版本号控制:⼀般是在数据表中加上⼀个数据版本号 version 字段,表⽰数据被修改的次数。当数据被修改时,version 值会 +1。
当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的version 值相等时才更新,否则重试更新操作,直到更新成功。
2 说明
乐观并发控制相信事务之间的数据竞争(data race)的概率是⽐较⼩的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产⽣任何锁和死锁。
四、具体实现
1 悲观锁实现⽅式
悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
1. 在对记录进⾏修改前,先尝试为该记录加上排他锁(exclusive locks)。
2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应⽅式由开发者根据实际需要决定。
3. 如果成功加锁,那么就可以对记录做修改,完成后就会解锁了。
4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
以举例,说明 SQL 中悲观锁的应⽤
要使⽤悲观锁,必须关闭 数据库的⾃动提交属性set autocommit=0。因为 MySQL 默认使⽤ autocom
mit 模式,也就是说,当执⾏⼀个更新操作后,MySQL 会⽴刻将结果进⾏提交。
以电商下单扣减库存的过程说明⼀下悲观锁的使⽤:
在对 id = 1 的记录修改前,先通过 的⽅式进⾏加锁,然后再进⾏修改。这就是⽐较典型的悲观锁策略。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论