在学习JUC的时候,其实我们一直都是围绕着如果解决多线程对共享资源访问造成可见性的问题
在操作系统中由多CPU和三级缓存带来的可见性问题;Java中由多线程带来可见性的问题,其中:
-
🚩
操作系统
会通过窥探技术
和缓存一致性协议
来解决上述问题 -
🚩JUC通过Java并发编程三大特征来解决上述问题
-
操作的
原子性
-
操作结果的
可见性
-
指令的
有序性
-
在MySQL中,同样存在多个线程操作同一份数据源的现象,我们通常将一个线程或者一个连接称之为一个事物
,MySQL主要通过ACID
来解决多事物可见性的问题
首先我们得来好好看一下数据库中的事物
思维导图:
事务的定义很严格,它必须同时满足四个特性,即
原子性
、一致性
、隔离性
和持久性
,也就是人们俗称的 ACID 特性,具体如下
-
原子性(Atomic)
表示将事务中所进行的操作捆绑成一个不可分割的单元,即对事务所进行的数据修改等操作,要么全部执行,要么全都不执行
**数据库是如何保证事物的原子性的?**通过三个
关键字
commit提交 rollback回滚 savepoint保存点
**Java是如何保证事物的原子性的?**通过三个
方法
connection.setAutoCommit(false)//关闭自动事物提交,因为JDBC中隐式事物提交的,即默认提交 connection.commit()//手动提交事物 connetion.rollback()//回滚
-
一致性(Consistency)
表示事务完成时,必须使所有的数据都保持一致状态。比如转账,有人转出就必须要保证有人转入
-
隔离性(Isolation)
指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
多个线程访问数据库中如何保证数据库中数据的可见性?
Java多线程下有可见性问题,操作系统多核开发时也有可见性问题,数据库里这样的问题用
隔离性
描述,其实道理都是相通的,每个线程访问数据库时,都有一份复制的临时数据,数据库对此设置了四种事物的隔离级别
,分别是:Read Uncommitted(读未提交)
、Read Committed(读已提交)
、Repeatable Read(可重复读取)
、Serializable(可串行化)
。在下面会专门来描述一下。 -
持久性(Durability)
持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。提交后的其他操作或故障不会对其有任何影响。
在实际应用中,数据库中的数据是要被多个用户共同访问的,在多个用户同时操作相同的数据时,可能就会出现一些事务的并发问题,具体如下
-
不可重复读
指一个事务对同一行数据重复读取两次,但得到的结果不同。例如A线程两次读取同一条数据,但是在第二次读取前,其他线程对其进行了修改,这就导致A线程两次读取数据的不一致。这里其他线程的操作是
update
更新 -
虚读/幻读
指一个事务执行两次查询,
但第二次查询的结果包含了第一次查询中未出现的数据
。这里其他线程的操作是insert
插入 -
丢失更新
指两个事务同时更新一行数据,后提交(或撤销)的事务将之前事务提交的数据覆盖了
丢失更新可分为两类,分别是
第一类丢失更新
和第二类丢失更新
。- 第一类丢失更新是指两个事务同时操作同一个数据时,当第一个事务
撤销
时,把已经提交的第二个事务的更新数据覆盖了,第二个事务就造成了数据丢失。 - 第二类丢失更新是指当两个事务同时操作同一个数据时,第一个事务将修改结果成功
提交
后,对第二个事务已经提交的修改结果进行了覆盖,对第二个事务造成了数据丢失。
- 第一类丢失更新是指两个事务同时操作同一个数据时,当第一个事务
为了避免上述事务并发问题的出现,在标准的 SQL 规范中定义了
四种事务隔离级别
,不同的隔离级别对事务的处理有所不同。这四种事务的隔离级别如下(其安全性由低到高、效率由高到低):
一个事务在执行过程中,既可以访问其他事务未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。此隔离级别可防止丢失更新,不能防止其他三种异常
一个事务在执行过程中,既可以访问其他事务成功提交的新插入的数据,又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读
一个事务在执行过程中,可以访问其他事务成功提交的新插入的数据,但不可以访问成功修改的数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。此隔离级别可有效防止不可重复读和脏读
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。此隔离级别可有效防止脏读、不可重复读和幻读
。但这个级别可能导致大量的超时现象和锁竞争,在实际应用中很少使用
Serializable 是一致性最好的,性能最差的,Read uncommitted是一致性(隔离性)最差的,性能最好的。一般不会使用
Serializable
和Read uncommitted
这两种隔离级别。一般来说,事务的隔离级别越高,越能保证数据库的完整性和一致性,但相对来说,隔离级别越高,对并发性能的影响也越大。因此,通常将数据库的隔离级别设置为Read Committed
,即读已提交数据,它既能防止脏读,又能有较好的并发性能。虽然这种隔离级别会导致不可重复读、幻读和第二类丢失更新这些并发问题,但可通过在应用程序中采用悲观锁和乐观锁加以控制
- oracle、sqlServer 读已提交(READ COMMITTED )
- mysql 可重复读(REPEATABLE-READ)
mysql 可以通过下面的语句来查询数据库的默认隔离级别(查询了全局事物隔离级别和会话事物隔离级别):
select @@global.transaction_isolation,@@transaction_isolation;
当然也可以手动修改数据库的事物隔离级别
- 不同数据库(跨数据源)之间统一事物管理,使用
JTA
处理 - 相同数据库(但数据源)管理事物,使用
JDBC
MVCC多版本并发控制(
Multi-Version Concurrency Control
),是在读取数据时通过一种类似快照的方式将数据保存下来,这样读锁
和写锁
不冲突了,不同的事物session
会看到自己特定版本的数据,我们称之为版本链
MVCC的好处是什么?最大的好处就是读写操作可以并发进行,极大的提升了效率
我们来看一下MySQL中可能会发生的并发问题:
数据库并发场景有三种,分别为:
读-读
:不存在任何问题,也不需要并发控制读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
在MVCC之前,例如MyISAM引擎只能通过加锁的方式来解决并发问题,但是这样会导致性能不佳
有了 MVCC,所以我们可以形成两个组合:
MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突MVCC + 乐观锁
MVCC 解决读写冲突,乐观锁解决写写冲突
我们知道数据库有四个隔离级别,其中MVCC
只在READ COMMITTED
和PEPEATABLE READ
两个隔离级别下工作,也就是读已提交和可重复读两个隔离级别
Read Uncommitted
读未提交为了读取到其他事物未提交的数据,总是会读取最新的数据行,而不是符合当前事物版本的数据行
Serializable
已经对所有并发操作的数据行都进行加锁了,已经没有并发问题了,当然也不需要MVCC了
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突
,它的实现原理主要是依赖记录中的 3个隐式字段
,undo日志
,Read View
来实现的。
隐藏字段中其实只有两个是必要的,分别是trx_id
和roll_pointer
- trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事物Id
- roll_pointer:每次对那条索引记录上有修改的时候,都会把老版本写入
undo日志
中。这个roll_pointer
就是存了一个指针,它指向这条索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息(注意插入操作的undo日志没有这个属性,因为它没有老版本)
其中
roll_pointer
将多个版本之间的信息通过指针连起来,形成一条版本链
下面我们来看一下Read View
的概念,当我们将版本链和read view这两个概念搞清楚了,我们也就能够弄明白MVCC的工作过程了