转:数据库的乐观锁和悲观锁

InnoDB存储引擎是基于事务的。而前面博文所讲的MyISAM存储引擎是不支持事务的。

在默认的情况下,MySQL从自动提交(autocommit)模式运行,这种模式会在每条语句执行完毕后把它作出的修改立刻提交给数据库并使之永久化。事实上,这相当于把每一条语句都隐含地当做一个事务来执行。如果你想明确地执行事务,需要禁用自动提交模式并告诉MySQL你想让它在何时提交或回滚有关的修改。执行事务的常用办法是发出一条START TRANSACTION(或BEGIN)语句挂起自动提交模式,然后执行构成本次事务的各条语句,最后用一条COMMIT语句结束事务并把它们作出的修改永久性地记入数据库。万一在事务过程中发生错误,用一条ROLLBACK语句撤销事务并把数据库恢复到事务开始之前的状态。START TRANSACTION语句"挂起"自动提交模式的含义是:在事务被提交或回滚之后,该模式将恢复到开始本次事务的START TRANSACTION语句被执行之前的状态。(如果自动提交模式原来是激活的,结束事务将让你回到自动提交模式;如果它原来是禁用的,结束当前事务将开始下一个事务。)如果是autocommit模式 ,autocommit的值应该为 1;请在试验前 确定autocommit 的模式是否开启 。

  • 1、事务及其ACID属性
    原子性,隔离性,持久性,一致性
  • 2、并发事务处理带来的问题
    更新丢失:一个事物覆盖了另一个事务的更新
    脏读:一个事物读到了另一个事务未提交的数据。
    不可重复读:一个事物的两次读期间,另一个事务改变了值
    幻读:一个事物两次读期间,另一个事务插入新的数据。
  • 3、事物的隔离级别
    脏读,不可重复读,幻读都是数据库读一致性问题,必须由数据库提供一种事务隔离机制来解决。数据库实现事务隔离的方式,有两种:
    一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
    一种是不加锁,通过生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别的一致性读取。这种技术叫做数据多版本并发控制,也经常成为多版本数据库(MVCC)。高低水位 redo undo log 的相关知识。

基于元数据的 Spring 声明性事务 : Isolation 属性一共支持五种事务设置,具体介绍如下:未提交读,已提交读,可重复读,可序列化 DEFAULT 使用数据库设置的隔离级别
( 默认 ) ,由 DBA 默认的设置来决定隔离级别 . READ_UNCOMMITTED 会出现脏读、不可重复读、幻读 ( 隔离级别最低,并发性能高 ) READ_COMMITTED 会出现不可重复读、幻读问题(锁定正在读取的行) REPEATABLE_READ 会出幻读(锁定所读取的所有行) SERIALIZABLE 保证所有的情况不会发生(锁表)

  • 4、InnoDB的行锁模式及其加锁方法
    InnoDB中有以下两种类型的行锁:共享锁和排他锁。
    对于update,insert,delete语句,InnoDB会自动给设计的数据集加排他锁。对于普通的select语句,InnoDB不会加任何锁。
    事物可以通过以下语句显示给记录集加共享锁和排他锁。
    共享锁:select * from 。。。。。 lock in share mode(不能加排他锁可以加共享锁 可以正常查询 Mysql普通查询不加锁)
    排它锁:select * from … for update(不能加其他的共享锁和排他锁 但可以正常查询 Mysql普通查询不加锁)

5、InnoDB行锁的实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。InnoDB这种行锁实现特点意味着:如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。
(1)在不通过索引条件查询时,InnoDB会锁定表中的所有记录。
(2)Mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果使用相同的索引键,是会出现冲突的。
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,但都是通过行锁来对数据加锁。即如果使用不同的索引看是否锁定了同一行数据。

悲观锁

当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。

定义:这种借助数据库锁机制在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。悲观锁认为并发下同一条数据被修改的可能性比较大。处理加锁的机制会让数据库并发效率降低,浪费CPU、服务器资源(一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。),还有增加产生死锁的机会。

悲观锁实现方式

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  • 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  • 其间如果有其他事务对该记录做加锁的操作,都要等待当前事务解锁或直接抛出异常。
    我们拿比较常用的MySql Innodb引擎举例,来说明一下在SQL中如何使用悲观锁。

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

如电商下单过程中扣减库存(扣减某一类产品的商品数量)的需求说明一下如何使用悲观锁:

//0.开始事务
begin; 
//1.查询出商品库存信息
select quantity from items where id=1 for update;
//2.修改商品库存为2
update items set quantity=2 where id = 1;
//3.提交事务
commit;

在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁**把整张表锁住,这点需要注意。

乐观锁

乐观锁( Optimistic Locking ) 适用于读多写少,线程竞争比较低的时候。不依赖于数据库锁,所以不会产生任何数据库锁和死锁,可以充分利用cpu的资源,提高了系统并发能力。

乐观锁实现方式
跟java 偏向锁和读写锁中的乐观锁的实现类似,在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,即CAS(ABA的问题可以检测版本号)。

还以下单为例

//查询出商品库存信息,quantity = 3
select quantity from items where id=1
//修改商品库存为2
update items set quantity=2 where id=1 and quantity = 3;

我们在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则更新,否则不更新。

通过版本号(自增的版本号或时间戳)解决ABA问题

//查询出商品信息,version = 1
select version from items where id=1
//修改商品库存为2
update items set quantity=2,version = 3 where id=1 and version = 2;

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少

以上SQL其实还是有一定的问题的,就是一旦高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减小乐观锁的粒度的。

//修改商品库存
update item 
set quantity=quantity - 1 
where id = 1 and quantity - 1 > 0

如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制。
以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

遇到的一个实际业务中遇到了一个问题:通过不同的线程从Es中(前台js)取出当日当班不同字段(A良品、A不良等)数据,判断当日当班数据是否存在,通过调用同一个后端接口,如果存在就插入;不存在则更新,而且不同线程更新该条数据的不同字段。这个过程中会出现并发的情况。

问题1:当日当班数据不会出现两次,数据库却出现了
分析原因:两个线程同时判断不存在当日当班数据,进行插入,就会插入两条一样的数据
解决方案:
(1)将当日当班两个字段(归属日和班别)设置联合主键,设置成唯一索引,那么插入时只会有一条数据插入成功,即使多个客户端也不会出现这种情况。
(2)后端接口代码层面加锁:读取的时候使用读锁,写的时候:插入或者更新的时候使用乐观锁(因为重复数据出现的不频繁,所以竞争不激烈,代码层面加乐观锁就可以解决)。
(3)将要更新或者插入的数据放入队列中。
问题2:更新数据时本该逐步增大的产量数据,时大时小。
原因:因为不同线程的并发,不同数据的不同字段第一次和第二次读取一次大一次小,大的先更新,然后更新小的。
解决方案:使用数据库乐观锁来实现
update item set quantity=newQuantity where id = 1 and quantity < newQuantity

如何选择

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

1、乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。

如何减少死锁

1.使用尽量少的长事务
2.事务访问表的顺序一致
3.使用索引,细化锁的粒度,防止锁表
4.代码加锁

目前,我们已经探讨了许多关于数据库锁的问题,锁能够有效地解决并发的问题,但这也带来了一个严重的缺点,那就是死锁。

死锁在操作系统中指的是两个或两个以上的进程在执行的过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

在操作系统中,死锁的处理是一个重要的话题,也已经有较为成熟的解决方法,如银行家算法等,在这边我们就不再阐述,只讨论数据库中的死锁。

数据库中常见的死锁原因与解决方案有:

  1. 事务之间对资源访问顺序的交替
    出现原因:
    一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
    解决方法:
    这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源

  2. 并发修改同一记录
    出现原因:主要是由于没有一次性申请够权限的锁导致的。参考:记录一次死锁排查过程
    用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁比较隐蔽,但在稍大点的项目中经常发生。
    解决方法:
    a. 乐观锁,实现写-写并发
    b. 悲观锁:使用悲观锁进行控制。悲观锁大多数情况下依靠数据库的锁机制实现,如Oracle的Select … for update语句,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

  3. 索引不当导致的死锁
    出现原因:
    如果在事务中执行了一条不满足条件的语句,执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
    另外一种情况是由于二级索引的存在,上锁的顺序不同导致的,这部分在讨论索引时会提到。参考:https://www.cnblogs.com/LBSer/p/5183300.html

解决方法:
SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
那么,如何尽可能的避免死锁呢?
1)以固定的顺序访问表和行。即按顺序申请锁,这样就不会造成互相等待的场面。
2)大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
3)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
4)降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
5)为表添加合理的索引。如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值