并发性事务带来的问题+并发控制(MySQL事务隔离级别、MySQL 锁)
在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)。
锁定义:为了解决并发事务带来的资源抢占问题而诞生的,主要是确保数据库中,多条工作线程并行执行时的数据安全性。
事务
事务:最小的不可再分的工作单元,逻辑上是一组操作,要么都执行,要么都不执行。
事务执行出错了就需要rollback回滚,事务处理是一种对必须整批执行的 MySQL 操作的管理机制。
在开启事务之后,执行sql不会立即去执行,只有等到commit操作后才会统一执行(保证原子性)。
因为某一刻不可能总只有一个事务在运行,如果没有采用及时的隔离机制,就会产生并发异常。
并发事务会带来哪些问题?
-
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
-
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
-
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
总结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
为了避免以上出现的各种并发问题,我们就必然要采取一些手段。mysql数据库系统提供了四种事务的隔离级别,用来隔离并发运行各个事务,使得它们相互不受影响,这就是数据库事务的隔离性。
并发控制——解决并发问题
并发控制是指在多进程同时访问数据库时,保证数据库操作的一致性和正确性的一种机制。它解决了在并发访问下可能出现的数据不一致、丢失更新和脏读等问题。
并发控制技术:
- 事务隔离级别(Isolation Levels): 数据库提供了多种隔离级别,如读未提交、读已提交、可重复读和串行化,用于控制事务之间的可见性。
- 锁(Locking): 使用锁机制可以控制对数据的访问,包括共享锁和排他锁,确保在某个事务对数据进行读取或修改时其他事务无法对其进行修改。
- 多版本并发控制(MVCC): 通过保存不同版本的数据来解决并发问题,允许事务在读取数据时不会被其他事务的写操作所阻塞。
- 时间戳(Timestamping): 使用时间戳来记录事务的开始和结束时间,以及数据的版本信息,通过比较时间戳来判断事务的执行顺序。
- 并发控制算法(Concurrency Control Algorithms): 包括基于事务的、基于锁的、基于时间戳的等不同算法来确保并发控制的正确性和效率。
MySQL事务隔离级别
一个事务的执行,本质上就是一条工作线程在执行,当出现多个事务同时执行时,这种情况则被称之为并发事务,所谓的并发事务也就是指多条线程并发执行。
多线程并发执行自然就会出问题,也就是脏写、脏读、不可重复读及幻读问题。而对于这些问题又可以通过调整事务的隔离级别来避免,那为什么调整事务的隔离级别后能避免这些问题产生呢?这是因为不同的隔离级别中,工作线程执行SQL语句时,用的锁粒度、类型不同。

用例子说明各个隔离级别的情况
1、读未提交:
- (1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值
- (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account400-50=350
- (3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据
- (4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据
2、读已提交
- (1)打开一个客户端A,并设置当前事务模式为read committed(不可重复读),查询表account的所有记录
- (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account
- (3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题
- (4)客户端B的事务提交commit
- (5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3、可重复读
- (1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录400
- (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account400-50=350
并提交 - (3)在客户端A查询表account的所有记录,与步骤(1)查询结果400一致,没有出现不可重复读的问题
- (4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,这里的balance值用的是步骤2中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,
select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。 - (5)重新打开客户端B,插入一条新数据后提交
- (6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读(在这种情况下不会幻读)
如果在先更新然后快照读的形式,就会出现幻读。事务A执行update account set balance=888 where id = 4;再次查询,此时事务A将新增的记录给查询出来了。或者使用共享锁当前读:SELECT * FROM account lock IN SHARE MODE;就会产生幻读。
总之:可重复读是没有完全解决幻读的问题。如果在同一个事务里面,只是总是执行普通的select快照读,是不会产生幻读的。但是如果在这个事务里面通过当前读或者先更新然后快照读的形式来读取数据,就会产生幻读。
4.串行化(serializable)
- (1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:
- (2)打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败,mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
补充:Mysql事务隔离级别与对应的锁机制 这篇讲得很细
- 1、事务隔离级别为读提交时,写数据只会锁住相应的行
- 2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
- 3、事务隔离级别为串行化时,读写数据都会锁住整张表
- 4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
当前读、快照读、MVCC
(1)当前读:
select lock in share mode(共享锁)
select for update(排他锁)
update(排他锁)
insert(排他锁)
delete(排他锁)
当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。
(2) 快照读
单纯的select操作,不包括上述 select … lock in share mode, select … for update。
Read Committed隔离级别:每次select都生成一个快照读。
Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。
(3)MVCC是Multi-Version Concurremt Control的简称,意思是基于多版本的并发控制协议,通过版本号,避免同一数据在不同事务间的竞争,只存在于InnoDB引擎下。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。它包括了快照读在内的多种读取方式,同时也处理写操作,通过版本管理来实现事务的隔离性。
数据库并发有以下几种场景:
读-读:不存在任何问题。
读-写:有线程安全问题,可能出现脏读、幻读、不可重复读。
写-写:有线程安全问题,可能存在更新丢失等。
mvcc 解决的就是读写时的线程安全问题,线程不用去争抢读写锁,使用快照读。
例如:事务A对数据加上写锁,事务B去读数据不成功,要等到A结束,而当有了mvcc,事务B区查询数据时,就使用快照读,查询到的是A修改前的数据,提高了数据库的并发效率。
快照读,也就是普通的select语句。快照读在读写时不用加锁,不过可能会读到历史数据。
还有一种读取数据的方式是当前读,是一种悲观锁的操作。它会对当前读取的数据进行加锁,所以读到的数据都是最新的。
(1.回顾事务的特性
原子性:通过undolog实现。事务回滚
持久性:通过redolog实现。
隔离性:通过**加锁(当前读)&MVCC(快照读)**实现。
一致性:通过undolog、redolog、隔离性共同实现。
在读已提交(RC)和可重复读(RR)隔离级别下的快照读,都是基于MVCC实现的!
mvcc如何实现RC和RR的隔离级别:
(1)RC的隔离级别下,每个快照读都会生成并获取最新的readview。
(2)RR的隔离级别下,只有在同一个事务的第一个快照读才会创建readview,之后的每次快照读都使用的同一个readview,所以每次的查询结果都是一样的
MVCC详解,深入浅出简单易懂
MySQL的锁体系:
这篇讲得超详细——MySQL锁、加锁机制(超详细)
以锁粒度的维度划分
全局锁:锁定数据库中的所有表。加上全局锁之后,整个数据库只能允许读,不允许做任何写操作
表级锁:每次操作锁住整张表。主要分为三类
表锁(分为表共享读锁 read lock、表独占写锁 write lock)
元数据锁(meta data lock,MDL):基于表的元数据加锁,加锁后整张表不允许其他事务操作。这里的元数据可以简单理解为一张表的表结构
意向锁(分为意向共享锁、意向排他锁):这个是InnoDB中为了支持多粒度的锁,为了兼容行锁、表锁而设计的,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查
行级锁:每次操作锁住对应的行数据。主要分为三类
记录锁 / Record 锁:也就是行锁,一条记录和一行数据是同一个意思。防止其他事务对此行进行update和delete,在 RC、RR隔离级别下都支持
间隙锁 / Gap 锁:锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持
临键锁 / Next-Key 锁:间隙锁的升级版,同时具备记录锁+间隙锁的功能,在RR隔离级别下支持
以互斥性的角度划分
共享锁 / S锁:不同事务之间不会相互排斥、可以同时获取的锁
排他锁 / X锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁
共享排他锁 / SX锁:MySQL5.7版本中新引入的锁,主要是解决SMO带来的问题
以操作类型的维度划分
读锁:查询数据时使用的锁
写锁:执行插入、删除、修改、DDL语句时使用的锁
以加锁方式的维度划分
显示锁:编写SQL语句时,手动指定加锁的粒度
隐式锁:执行SQL语句时,根据隔离级别自动为SQL操作加锁
以思想的维度划分
乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁
悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行
放眼望下来,是不是看着还蛮多的,但总归说来说去其实就共享锁、排他锁两种,只是加的方式不同、加的地方不同,因此就演化出了这么多锁的称呼。
1.共享锁【S锁】
数据库自带的锁。
又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
(数据加上S锁,所有线程只能读)
上共享锁的写法:lock in share mode
例如: select math from zje where math>60 lock in share mode;
2.排他锁【X锁】
数据库自带的锁。
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
确保不会同时同一资源进行多重更新。
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
(数据加上X锁,只能当前线程对数据进行读写,其他不能线程读写)
上排它锁的写法:for update
例如:select math from zje where math >60 for update;
对比
- 共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
- 排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
排它锁和共享锁实例
ReentrantLock就是一种排它锁。CountDownLatch是一种共享锁。这两类都是单纯的一类,即,要么是排它锁,要么是共享锁。 ReentrantReadWriteLock是同时包含排它锁和共享锁特性的一种锁,这里主要以ReentrantReadWriteLock为例来进行分析学习。我们使用ReentrantReadWriteLock的写锁时,使用的便是排它锁的特性;使用ReentrantReadWriteLock的读锁时,使用的便是共享锁的特性。
3.乐观锁
乐观锁不是数据库自带的,需要我们自己实现。
乐观锁是指更新数据库想法很乐观,在操作数据时不加锁,而在更新后再去判断是否有冲突。
使用乐观锁需修改数据库的事务隔离级别:
使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)。
(1)版本号法实现乐观锁
在商品表中增加一个版本号字段,每次更新库存时,都会携带这个版本号。如果版本号没有发生变化,说明在此期间没有其他线程修改过数据,更新操作可以进行;如果版本号发生了变化,则说明有其他线程已经修改过数据,此时需要重新获取最新的数据并尝试再次更新。
"UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock > 0 AND version = #{version}
//当商品的库存大于0且版本号与传入的版本号相同时,将库存减1,并将版本号加1。
(2)CAS法实现乐观锁
CAS 用库存量代替版本号,在执行操作先判断 where 用的库存量和在最初查询的时候是否相等。
加乐观锁: 先比较再更新,更新的时候判断**此时的库存是否是之前查询出的库存,**如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新。
CAS
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
- CAS的自旋:
自旋: 就是不停的判断比较,看能否将值交换
在CAS操作中,自旋的概念指的是当线程发现无法立即完成操作时,不会让出CPU时间片,而是继续循环尝试,直到成功为止。这种机制可以避免线程频繁地挂起和恢复,减少了线程切换的开销,提高了效率。自旋锁通常适用于锁被占用的时间较短的场景,因为长时间的自旋会导致CPU资源的浪费。
总的来说,CAS的自旋是通过不断循环尝试来实现的一种锁优化机制,它在多线程编程中用于保证操作的原子性和提高性能。
- CAS/乐观锁存在的问题(ABA):
CAS 三大问题: - (1)ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。
ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从A-B-A变成了1A-2B-3A。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类型。 - (2)循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
乐观锁在高并发场景下的问题:
乐观锁在高并发场景下的问题,是严重的空自旋
自旋: 就是不停的判断比较,看能否将值交换
4.悲观锁
悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。
另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
适用场景:
- 悲观锁适合写操作多的场景。
- 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法:
select for update,提前加上写锁
beginTranse(开启事务)
try{
query('select amount from s_store where goodID = 12345 for update');//加上写锁
if(库存 > 0){
//quantity为请求减掉的库存数量
query('update s_store set amount = amount - quantity where goodID = 12345');
}
}catch( Exception e ){
rollBack(回滚)
}
commit(提交事务)
5.意向锁
意向锁来快速判断是否可以对某个表使用表锁
意向锁是表级锁,共有两种:意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁之间是互相兼容的。
意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。
5.行锁
行锁,由字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。
6.表锁
对表操作的,所以自然锁住全表的表锁就不会出现死锁。但是表锁效率低。
7.ReentrantLock
ReentrantLock是一种可重入的互斥锁,提供公平锁和非公平锁的选择,支持中断和超时。其优点包括可中断、可设置超时、多条件绑定等。文章介绍了ReentrantLock的使用注意事项,如避免锁粒度过大、保持加锁顺序防止死锁,并提供了实际应用案例,包括处理方法超时和演示公平锁与非公平锁的区别。
8.AQS
AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。
AQS 就是一个抽象类,主要用来构建锁和同步器。
Spring事务管理
为了解决并发事务带来的问题而诞生的,主要是确保数据库中,多条工作线程并行执行时的数据安全性。
事务并发:多个线程同时对一个资源进行读写操作,要保证它的一致性和隔离性。
Spring 管理事务的方式有几种?
- 编程式事务:在代码中硬编码(在分布式系统中推荐使用) : 通过
TransactionTemplate或者TransactionManager手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。 - 声明式事务:在 XML 配置文件中配置或者直接基于注解(单体应用或者简单业务系统推荐使用) : 实际是通过 AOP 实现(基于
@Transactional的全注解方式使用最多)
注意:事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的 innodb引擎。但是,如果把数据库引擎变为 myisam,那么程序也就不再支持事务了!
注:
(1)也就是说解决并发问题而引起的数据的不一致性,可以对数据加锁,也可以使用Spring 事务管理。
更多推荐
所有评论(0)