数据库悲观锁和乐观锁

mysql的锁

mysql中不同的引擎分不同的锁

行锁 表锁 页锁
MyISAM
BDB
InnoDB
  • 表锁: 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低

  • 行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高(innodb一般都是用行锁)

  • 页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般

因为我们全是用的innodb所以我们忽略页锁

回顾事务以及acid的属性

1.事务(Transaction)及其ACID属性 事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发带来的问题

 相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。

  更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问 题。   脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加 控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。   不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。   幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

2、事务并发会产生问题例子:

1)第一类丢失更新:在没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出, 导致对数据的两个修改都失效了。

例如:

张三的工资为5000,事务A中获取工资为5000,事务B获取工资为5000,汇入100,并提交数据库,工资变为5100,

随后

事务A发生异常,回滚了,恢复张三的工资为5000,这样就导致事务B的更新丢失了。

2)脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。 例如:   张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。   与此同时,   事务B正在读取张三的工资,读取到张三的工资为8000。   随后,   事务A发生异常,而回滚了事务。张三的工资又回滚为5000。   最后,   事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。

3)不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。 例如:   在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。   与此同时,   事务B把张三的工资改为8000,并提交了事务。   随后,   在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。

4)第二类丢失更新:不可重复读的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

例如:

在事务A中,读取到张三的存款为5000,操作没有完成,事务还没提交。   与此同时,   事务B,存储1000,把张三的存款改为6000,并提交了事务。   随后,   在事务A中,存储500,把张三的存款改为5500,并提交了事务,这样事务A的更新覆盖了事务B的更新。

5)幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。 例如:   目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。   此时,   事务B插入一条工资也为5000的记录。   这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。

提醒: 不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了 幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样

事务隔离级别

  在上面讲到的并发事务处理带来的问题中,“更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。

  一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。

  另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一 致 性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

  • 当前读 像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

    另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一 致 性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

  • 快照读 像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

  数据库的事务隔离越严格,并发副作用越 小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏 感,可能更关心数据并发访问的能力。

为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每个级别的隔离程度不同,允许出现的副作用也不同,应用可以根据自己的业务逻辑要求,通过选择不同的隔离级别来平衡 “隔离”与“并发”的矛盾。一下是这4个隔离级别的特性。

读数据一致性及允许的并发副作用 隔离级别 读数据一致性 脏读 不可重复读 幻读
未提交读(Read uncommitted) 最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed) 语句级
可重复读(Repeatable read) 事务级
可序列化(Serializable) 最高级别,事务级

mysql默认的事务处理级别是'REPEATABLE-READ',也就是可重复读

设置可序列化transaction-isolation = SERIALIZABLE

但是为什么我们不用这个可序列化呢,原因是:在这个模式下事务串行化顺序执行,也就是事物a 执行读写操作时,会锁定检索的数据行范围(范围锁),这种锁会阻止其他事物在本范围内的一切操作,这种事务隔离级别效率低下,比较耗数据库性能,对并发也不友好,虽然并发数据准但是速度太慢了。

悲观锁和乐观锁

悲观锁和乐观锁两种常见的资源并发锁设计思路,也是并发编程中一个非常基础的概念

mysql的并发操作时而引起的数据的不一致性(数据冲突):

解决方案:

1.悲观锁,假设两个用户(或以上)对同一个数据对象操作引起的数据出问题。一定存在 利用数据库的一种机制。

2.乐观锁,假设两个用户(或以上)对同一个数据对象操作引起的数据出问题。不一定发生。update时候存在版本,更新时候按版本号进行更新。

所以说当涉及到并发改数据库的情况,我们这边要去使用悲观锁或者是乐观锁

什么是悲观锁

java里面的同步机制synchronized关键字就是一个悲观锁,当一个变量或者是方法使用了synchronized修饰时,其他的线程想要拿到这个变量或者是方法的时候将就需要等到别的线程释放。

悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了

悲观锁一般分两个

共享锁,又称之为读锁,简称S锁,当事务对数据加上读锁后,其他事务只能对该数据加读锁,不能做任何修改操作,也就是不能添加写锁。只有当数据上的读锁被释放后,其他事务才能对其添加写锁。共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而避免”不可重读”的问题的出现。人话:我读的时候别人能读,但是不能改

实现方式:SELECT * from city where id = "1" lock in share mode;

排他锁,又称为写锁、独占锁,简称x锁,在我们开发使用的mysql中 INSERT、UPDATE 或 DELETE会默认进行排它锁但是select并不会加。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取锁和修改A。人话:我写的时候别人不能写

实现方式:但是这个是比较不好理解的,不能加锁并不代表不能读,select xx from xx 并不会加任何类型的锁,所以其他事务也可能读的但是当我们所以我们就使用SELECT * from city where id = "1" for update 就是加写锁,这个时候你读就读不到了,就必须等人家修改完你才能读的到,所以我们开发想使用悲观锁就用for update;

总结一下 如果你使用共享锁用select的时候别人不能改,但是能普通的读和加共享锁的读,当你是写锁select的时候别人不能改,但是只能普通的读不能加锁的读。反正就是加锁的就不能改,但是你普通的select 是不加锁的 别人能改,并且别人改了虽然事务没有提交你也能select到,但是如果你select for update了就是一但有人改了,你必须事务结束之后你才能读到,所以一般比较重要敏感的数据就要这样去弄,比如金额,但是这样去弄是有缺陷的,你要知道,只要加锁,运行的效率就会出问题,就会等。

什么是乐观锁

虽然较乐观锁,但是他是不加锁的

乐观锁是靠我们程序员去实现的

mysql的乐观锁实现有方式

版本号机制

数据库加version字段 每次更新给这个version+1,条件加上你之前的version,如果version给其他的线程改了,你这个请求就会被驳回。

mybatis实现这种方式要获取update影响条数,准确的影响条数要在连接地址上写useAffectedRows=true 类似于jdbc:mysql://localhost:3306/kj08?useAffectedRows=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT

手动回滚事务:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

还有一种乐观锁概念叫做CAS(compare and swap)

CAS:CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

        乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

        两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

        本质上,数据库的乐观锁做法和悲观锁做法主要就是解决下面假设的场景,避免丢失更新问题:
        一个比较清楚的场景
        下面这个假设的实际场景可以比较清楚的帮助我们理解这个问题:
假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是’有效’,表示该订单是有效的;
后台管理人员查询到这条001的订单,并且看到状态是有效的
用户发现下单的时候下错了,于是撤销订单,假设运行这样一条SQL: update order_table set status = ‘取消’ where order_id = 001;
后台管理人员由于在b这步看到状态有效的,这时,虽然用户在c这步已经撤销了订单,可是管理人员并未刷新界面,看到的订单状态还是有效的,于是点击”发货”按钮,将该订单发到物流部门,同时运行类似如下SQL,将订单状态改成已发货:update order_table set status = ‘已发货’ where order_id = 001

观点1:只有冲突非常严重的系统才需要悲观锁;“所有悲观锁的做法都适合于状态被修改的概率比较高的情况,具体是否合适则需要根据实际情况判断。”,表达的也是这个意思,不过说法不够准确;的确,之所以用悲观锁就是因为两个用户更新同一条数据的概率高,也就是冲突比较严重的情况下,所以才用悲观锁。


观点2:最后提交前作一次select for update检查,然后再提交update也是一种乐观锁的做法,的确,这符合传统乐观锁的做法,就是到最后再去检查。但是wiki在解释悲观锁的做法的时候,’It is not appropriate for use in web application development.’, 现在已经很少有悲观锁的做法了,所以我自己将这种二次检查的做法也归为悲观锁的变种,因为这在所有乐观锁里面,做法和悲观锁是最接近的,都是先select for update,然后update

在实际应用中我们在更新数据的时候,更严谨的做法是带上更新前的“状态”,如

update order_table set status = ‘取消’ where order_id = 001 and status = ‘待支付’ and ..........; 

update order_table set status = ‘已发货’ where order_id = 001 and status = ‘已支付’ and ..........;
然后在业务逻辑代码里判断更新的记录数,为0说明数据库已经被更新了,否则是正常的。

总结 如果遇到多并发的问题我们再java层也能解决,在mysql层也能解决,看我们的实现方式,你在java上玩锁也行,你在mysql上玩锁也行,我这边给你们提供概念,具体实现方式结合你们的项目,看需求实现

你可能感兴趣的:(Java,java,数据库,面试)