并发控制: 事务和锁的存在都是为了更好的解决并发访问造成的数据不一致性的的问题
乐观锁和悲观锁都是为了解决并发控制问题;
乐观锁可以认为是一种在最后提交的时候检测冲突的手段;
而悲观锁则是一种避免冲突的手段。
什么是乐观锁
是应用系统层面和数据的业务逻辑层次上的,利用程序处理并发,它假定当某一个用户去读取某一个数据时,其他的用户不会来访问修改这个数据,但在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小
实现方式
乐观锁的实现大部分都是基于版本控制实现, 此外,还可通过时间戳方式,通过提前读取,事后对比方式实现 。如mysql里添加版本version字段来控制
UPDATE price_versionSET front = #{front,jdbcType=DECIMAL},version= version + 1WHERE id = #{id,jdbcType=INTEGER}
乐观锁的优势和劣势
什么是悲观锁?
完全依赖数据库锁的机制实现,在数据库中可用Repeatable Read的隔离级别(可重复读)实现悲观锁。它认为当某一用户读取某一数据时,其他用户也会对该数据进行访问,所以在读取时就对数据加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但其他用户可读取该数据, 只有当自己读取完毕才释放锁。
悲观锁的优势和劣势
劣势:开销较大,且加锁时间较长,对于并发的访问性支持不好。
优势 : 能避免冲突的发生,
以hibernate为例,可通过为记录添加版本或时间戳字段来实现乐观锁,一旦发现出现冲突了,修改失败就要通过事务进行回滚。可用session.Lock()锁定对象来实现悲观锁(本质是执行SELECT * FROM t FOR UPDATE语句)
乐观锁和悲观所各有优缺点,在乐观锁和悲观锁之间进行选择的标准是:
- 发生冲突的频率与严重性。
- 如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。
- 如果冲突太多或者冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略,它能避免冲突的发生。 如果要求能够支持高并发,那么乐观锁。
其实使用乐观锁 高并发==高冲突, 看看你怎么衡量了。
但现在大多数源代码开发者更倾向于使用乐观锁策略
共享锁和排它锁是具体的锁,是数据库机制上的锁。
共享锁(读锁) 在同一个时间段内,多个用户可读取同一个资源,读取过程中数据不发生任何变化。读锁间是相互不阻塞的, 多个用户可同时读,但不允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁
排它锁(写锁) 在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他读锁或写锁操作,只能由这一个用户来写,其他用户既不能读也不能写。
加锁会有粒度问题,从粒度上从大到小可以划分为
开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。
MySQL中较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁间,所以获取锁定所需要的资源开销,及所能提供的并发处理能力也同样是介于上面二者间。另外,页级锁定和行级锁定一样,会发生死锁。
开销较大,能具体锁定到表中的某行数据,能更好的支持并发处理, 会发生死锁
所谓数据一致性,就是当多个用户试图同时访问一个数据库,它们的事务同时使用相同的数据时,可能会发生以下四种情况:
- 丢失更新、
- 脏读、
- 不可重复读
- 幻读
所谓数据完整性,数据库中的数据是从外界输入的,而数据的输入由于种种原因,会发生输入无效或错误信息。保证输入的数据符合规定,
数据完整性分为四类:
- 实体完整性(Entity Integrity)、
- 域完整性(Domain Integrity)、
- 参照完整性(Referential Integrity)、
- 用户定义的完整性(User-definedIntegrity)。
数据库采用多种方法来保证数据完整性,包括外键、约束、规则和触发器。
- 原子性Automicity,一个事务内的所有操作,要么全做,要么全不做
- 一致性Consistency,数据库从一个一致性状态转到另一个一致性状态
- 独立性(隔离性)isolation, 一个事务在执行期间,对于其他事务来说是不可见的
- 持久性(Durability): 事务一旦成功提交,则就会永久性的对数据库进行了修改
在SQL 中定义了四种隔离级别
READ UNCOMMITED(未提交读) 事务间的数据是相互可见的
READ COMMITED(提交读) 大多数数据库的默认隔离级别, 保证了不可能脏读,但不能保证可重复读, 在这个级别里,数据的加锁实现是读取都是不加锁的,但数据的写入、修改和删除是要加锁的。
REPEATABLE READ (可重复读) 解决了不可重复读问题,保证在同一个事务之中,多次读取相同的记录的值的结果是一致的。 但无法解决幻读。这个阶段的事务隔离性,在mysql中是通过基于乐观锁原理的多版本控制实现的。
SERIALIZABLE (可串行化读) 最高的隔离级别,解决了幻读 ,它在读取的每行数据上都加锁, 有能导致超时和锁争用的问题。
它的加锁实现是读取时加共享锁,修改删除更新时加排他锁,读写互斥,但并发能力差。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
Mysql 默认的隔离级别是可重复读 。
丢失更新: 当两个或多个事务同时对某一数据进行更新时,事务B的更新可能覆盖掉事务A的更新,导致更新丢失
解决方案:
- 悲观锁方式: 加锁,建议最后一步更新数据时加上排它锁,不要在一开始就加锁。执行到了最后一步更新,首先做加锁的查询确认数据有无改变,如没有被改变,则进行数据更新,否则失败。 一定要是做加锁的查询确认,因为如果不加锁,有可能在做确认时数据又发生了改变。
- 乐观锁的方式:使用版本控制实现
级别高低是&#xff1a;脏读 <不可重复读 <幻读。所以&#xff0c;设了最高级别的SERIALIZABLE_READ就不用在设置REPEATABLE_READ和READ_COMMITTED了
事务可读取未提交的数据&#xff0c;如&#xff1a; 事务A对某一个数据data&#61;1000 进行了修改: data &#61; 2000, 但是还没有提交&#xff1b; 事务B读取data 得到了结果data &#61; 2000, 由于某种原因事务A撤销了刚才的操作&#xff0c;数据data &#61; 1000 然后提交 这时事务B读取到的2000就是脏数据。正确的数据应该还是 1000
解决方法 &#xff1a; 把数据库的事务隔离级别调整到READ_COMMITTED &#xff0c; 但存在事务A与B都读取了data&#xff0c;A还未完成事务&#xff0c;B此时修改了数据data&#xff0c;并提交&#xff0c; A又读取了data&#xff0c;发现data不一致了&#xff0c;出现了不可重复读。
在同一个事务之中&#xff0c;多次读取相同的记录的值的结果是不一样的&#xff0c;针对的是数据的修改和删除。
事务A 读取data &#61; 1000, 事务还未完成&#xff1b; 事务B 修改了data &#61; 2000&#xff0c; 修改完毕事务提交&#xff1b; 事务A 再次读取data, 发现data &#61; 2000 了&#xff0c;与之前的读取不一致的
解决办法; 把数据库的事务隔离级别调整到 REPEATABLE READ &#xff0c; 读取时候不允许其他事务修改该数据&#xff0c;不管数据在事务过程中读取多少次&#xff0c;数据都是一致的&#xff0c;避免了不可重复读问题
幻读&#xff1a; 当某个事务在读取某个范围内的记录的时候&#xff0c;另外一个事务在这个范围内增加了一行&#xff0c;当前一个事务再次读取该范围的数据的时候就会发生幻行&#xff0c;. 针对的是数据的插入insert
解决方案 &#xff1a; 采用的是范围锁 RangeS RangeS_S模式&#xff0c;锁定检索范围为只读 或者 把数据库的事务隔离级别调整到SERIALIZABLE_READ&#xff0c; MySQL中InnoDB 和 XtraDB 利用&#xff08;多版本并发控制&#xff09;解决了幻读问题&#xff0c;
因为有大量的并发访问&#xff0c;为了预防死锁&#xff0c;一般应用中推荐使用一次封锁法&#xff0c;就是在方法的开始阶段&#xff0c;已经预先知道会用到哪些数据&#xff0c;然后全部锁住&#xff0c;在方法运行之后&#xff0c;再全部解锁。这种方式可以有效的避免循环死锁&#xff0c;但在数据库中却不适用&#xff0c;因为在事务开始阶段&#xff0c;数据库并不知道会用到哪些数据。
将事务分成两个阶段&#xff0c;加锁阶段和解锁阶段&#xff08;所以叫两段锁&#xff09;
这种方式虽然无法避免死锁&#xff0c;但是两段锁协议可以保证事务的并发调度是串行化&#xff08;串行化很重要&#xff0c;尤其是在数据恢复和备份的时候&#xff09;的。
**指两个事务或者多个事务在同一资源上相互占用&#xff0c;并请求对方所占用的资源&#xff0c;从而造成恶性循环的现象。 **
数据库也会发生死锁的现象&#xff0c;数据库系统实现了各种死锁检测和死锁超时机制来解除死锁&#xff0c;锁监视器进行死锁检测&#xff0c;MySQL的InnoDB处理死锁的方式是 将持有最少行级排它锁的事务进行回滚&#xff0c;相对比较简单的死锁回滚办法
避免死锁的核心思想是&#xff1a;
系统对进程发出每一个资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配.这是一种保证系统不进入不安全或者死锁状态的动态策略。 什么是不安全的状态&#xff1f;系统能按某种进程推进顺序( P1, P2, …, Pn)&#xff0c;为每个进程Pi分配其所需资源&#xff0c;直至满足每个进程对资源的最大需求&#xff0c;使每个进程都可顺序地完成。此时称 P1, P2, …, Pn 为安全序列。如果系统无法找到一个安全序列&#xff0c;则称系统处于不安全状态。
其实第一和第二是预防死锁的方式&#xff0c;分别对应着的是破坏循环等待条件&#xff0c;和破坏不可剥夺条件。
银行家算法&#xff1a;
思想&#xff1a; 当进程首次申请资源时&#xff0c;要测试该进程对资源的最大需求量&#xff0c;如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源&#xff0c;否则就推迟分配。当进程在执行中继续申请资源时&#xff0c;先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源&#xff0c;若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量&#xff0c;若能满足则按当前的申请量分配资源&#xff0c;否则也要推迟分配
主要是通过设置某些外部条件去破坏死锁产生的四个必要条件中的一个或者几个。
READ COMMITED 和 REPEATABLE READ 的隔离性实现&#xff1a;MVCC
MVCC&#xff08;多版本控制系统&#xff09;的实现&#xff08;目的&#xff1a; 实现更好的并发&#xff0c;可以使得大部分的读操作不用加锁&#xff0c; 但是insert&#xff0c;delete&#xff0c;update是需要加锁的&#xff09;&#xff1a;
MVCC 只在 READ COMMITED 和 REPEATABLE READ 这两个事务隔离性级别中使用。这是因为MVCC 和其他两个不兼容&#xff0c;READ UNCOMMITED 总是读取最新的行&#xff0c;不关事务&#xff0c; 而Seriablizable则会对每一个读都加共享锁。
在InnoDB中&#xff0c;会在每行数据后添加两个额外的隐藏的值来实现MVCC&#xff0c;这两个值一个记录这行数据何时被创建&#xff0c;另外一个记录这行数据何时过期&#xff08;即何时被删除&#xff09;。 在实际操作中&#xff0c;存储的并不是时间&#xff0c;而是系统的版本号&#xff0c;每开启一个新事务&#xff0c;系统的版本号就会递增。
通过MVCC&#xff0c;虽然每行记录都需要额外的存储空间&#xff0c;更多的行检查工作以及一些额外的维护工作&#xff0c;但可以减少锁的使用&#xff0c;大多数读操作都不用加锁&#xff0c;读数据操作很简单&#xff0c;性能很好&#xff0c;并且也能保证只会读取到符合标准的行&#xff0c;也只锁住必要行。
select &#xff08;不加锁&#xff09;&#xff1a; 满足两个条件的结果才会被返回&#xff1a;
MySQL中的读&#xff0c;和事务隔离级别中的读&#xff0c;是不一样的&#xff0c; 在REPEATABLE READ 级别中&#xff0c;通过MVCC机制&#xff0c;虽然让数据变得可重复读&#xff0c;但我们读到的数据可能是历史数据&#xff0c;是不及时的数据&#xff08;存储在缓存等地方的数据&#xff09;&#xff0c;不是数据库当前的数据&#xff01;这在一些对于数据的时效特别敏感的业务中&#xff0c;就很可能出问题。
在MVCC中&#xff1a;
快照读就是select &#xff0c;是不加锁的&#xff0c; 在REPEATABLE READ 和READ COMMITED 级别中 select语句不加锁。
select * from table ….;
当前读&#xff1a;插入/更新/删除操作&#xff0c;属于当前读&#xff0c;处理的都是当前的数据&#xff0c;需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
事务的隔离级别实际上都是定义了当前读的级别&#xff0c;MySQL为了减少锁处理&#xff08;包括等待其它锁&#xff09;的时间&#xff0c;提升并发能力&#xff0c;引入了快照读的概念&#xff0c;使得select不用加锁。而update、insert这些“当前读”&#xff0c;就需要另外的模块来解决了。&#xff08;这是因为update、insert的时候肯定要读取数据库中的值来与当前事务要写入的值进行对比&#xff0c;看看在该事务所处理的数据在这一段时间内有没有被其他事务所操作&#xff08;就是先读取数据库中数据的版本号与当前的版本号做检查&#xff09;&#xff09;
为了解决当前读中的幻读问题&#xff0c;MySQL事务使用了Next-Key锁。Next-Key锁是行锁和GAP&#xff08;间隙锁&#xff09;的合并 GAP&#xff08;间隙锁&#xff09;就是在两个数据行之间进行加锁&#xff0c;防止插入操作
行锁防止别的事务修改或删除&#xff0c;解决了数据不可重复读的问题&#xff0c;GAP锁防止别的事务新增&#xff0c;行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在读数据时的幻读问题
Serializable级别使用的是悲观锁的理论&#xff0c; 读加共享锁&#xff0c;写加排他锁&#xff0c;读写互斥&#xff0c; 在Serializable这个级别&#xff0c;select语句还是会加锁的。
https://blog.csdn.net/woshiluoye9/article/details/68954515