MySQL数据库基础入门与常见操作MySQL中数据库事务实践与锁详解 事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。

数据库事务是保证在并发情况下能够正确执行的重要支撑,MySQL常见的数据库引擎中支持事务的是InnoDB。

事务就是一系列操作,正确执行并提交,如果中途出现错误就回滚。事务要保证能够正常的执行,就必须要保持ACID特性。

事务由单独单元的一个或多个SQL语句组成,在这个单元中,每个MySQL语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条SQL语句一旦执行失败或产生错误,整个单元将会回滚。所有受到影响的数据将返回到事物开始以前的状态。如果单元中的所有SQL语句均执行成功,则事物被顺利执行。

事务的开始和结束

以第一个DML 语句的执行作为开始,以下面的其中之一作为结束:

  • COMMIT 或ROLLBACK 语句
  • DDL 或DCL 语句(自动提交)
  • 用户会话正常结束
  • 系统异常终了

关于DDL和DCL参考下图:
MySQL中事务详解_spring


【1】事务的四个特性

事务的四个关键属性(ACID)。

① 原子性(atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

事务是一个原子操作, 由一系列动作组成。 组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。

事务中的任何一个数据库操作失败,已经执行的任何操作都必须被撤销,让数据库返回初始状态。

② 一致性(consistency)

事务必须使数据库从一个一致性状态变换到另外一个一致性状态。

一旦所有事务动作完成, 事务就被提交。数据和资源就处于一种满足业务规则的一致性状态,即数据不会被破坏。

比如a+b=100,一个事务改变了a比如增加了a的值,那么必须同时改变b,保证在事务结束以后a+b=100依然成立,这就是一致性。

③ 隔离性(isolation)

事务的隔离性是指​​一个事务的执行不能被其他事务干扰​​,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准​​确地说,并非要求做到完全无干扰​​。

数据库规定了多种事务隔离界别,不同的隔离级别对应不用的干扰程度。隔离级别越高,数据一致性越好,但并发行越弱。比如对于A对B进行转账,A没把这个交易完成的时候,B是不知道A要给他转钱。

④ 持久性(durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

数据库管理系统一般采用​​重执行日志​​​来保证​​原子性、一致性和持久性​​。

重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即​​可根据重执行日志撤销已经执行的操作​​。对于已经提交的事务即使数据库崩溃,在重启数据库时也能根据日志对尚未持久化的数据进行相应的重执行操作。

【2】事务的传播属性

即当前的事务方法被另外一个事务方法调用时如何使用事务,​​默认取值为required,即使用调用方法的事务​​。

事务传播行为类型

说明

REQUIRED

如果有事务在运行,当前的方法就在这个事务内运行;否则,就启动一个新的事务,并在自己的事务内运行;

REQUIRES_NEW

当前的方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起。

MANDATORY

当前的方法必须运行在事务内部,如果没有正在运行的事务,将抛出异常。

SUPPORTS

如果有事务在运行,当前的方法就在这个事务内运行;否则它可以不运行在事务中。

NOT_SUPPORTED

当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。

NEVER

当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。

NESTED

如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行。

其中,最常使用的是 REQUIRED 、REQUIRES_NEW。

前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。 它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)

​ROPAGATION_REQUIRES_NEW​ 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.

另一方面, ​​PROPAGATION_NESTED​​开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务。潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint。 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.

PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:

PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 潜套事务也会被 commit, 这个规则同样适用于 roll back。


【3】事务的隔离级别

​一个事务与其他事务隔离的程度称为隔离级别​​。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。

数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题。

对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题。

五种隔离级别:​​DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE​​。

Oracle 支持的2 种事务隔离级别:​​READ_COMMITTED, SERIALIZABLE​​​。Oracle 默认的事务隔离级别为: ​​READ_COMMITTED​​。

MySQL支持4 种事务隔离级别:​​READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE​​​。MySQL 默认的事务隔离级别为: ​​REPEATABLE_READ​

① DEFAULT(读提交)

这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。大部分数据库的默认级别都是​​READ_COMMITTED​​(读取已提交)。


② READ_UNCOMMITTED(读取未提交)

这是​​事务最低的隔离级别,允许当前事务读取未被其他事务提交的变更​​​。这种隔离级别会产生​​脏读,不可重复读和幻读​​。

产生脏读场景:A事务读取一个字段,但是这个字段被另外一个事务更新却未提交,再次读取该字段时如果另外一个事务回滚则出现了脏读现象(读到的数据与第一次,数据库中的数据都不同)。

产生不可重复读场景:A事务读取一个字段,但是这个字段被另外一个事务更新并提交,再次读取该字段值不一样则出现了不可重复读现象(同一个事务中,不能保证读取的字段值相同)。

产生幻读场景:A事务读取一个字段集合,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。

或者说事务A开启事务,查询当前表-目标数据不存在;事务B开启事务插入一条新数据(id为主键);事务A 插入和事务B一样的数据,会出现锁等待超时现象。事务B提交后,事务A再次尝试插入,出现Duplicate entry…事务B很诧异,明明查询出来不存在将要插入的数据┭┮﹏┭┮


③ READ_COMMITTED(读取已提交)

保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。可以​​避免脏读,但不可重复读和幻读的现象仍然可能出现​​。

不可重复读

A事务读取一个字段,但是这个字段被另外一个事务更新并提交,再次读取该字段值不一样则出现了不可重复读现象(同一个事务中,不能保证读取的字段值相同)。

举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取到了A是50,刚读取完,A就被修改成100了,这个时候另一个事务再进行读取发现A就突然变成100了

幻读

读取一个字段,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。


④ REPEATABLE_READ(可重复读)

确保事务可以多次从某行记录的一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新。这种事务隔离级别​​可以防止脏读,不可重复读,但是可能出现幻读​​。

它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了在一个事务过程,读取的数据不会发生变化(即使数据库中的数据在该事务过程中发生了变化)。

//如下代码所示,可重复读意味着两次读取字段A值相同!

public void test(){
//开启事务
//读取字段A;
//此时数据库中A发生了变化;
//读取字段A;
//提交事务;
//关闭事务;
}

幻读

读取一个字段集合,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。


⑤ SERIALIZABLE :(可串行化)

在并发情况下和串行化的读取的结果是一致的,没有什么不同。这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。

除了防止脏读,不可重复读外,还避免了幻读。但性能十分低下!

⑥ 什么是脏读、不可重复读和幻读?

​脏读:​​​ 对于两个事务 T1, T2。T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, ​​T1读取的内容就是临时且无效的​​,也就是脏数据。

​不可重复读:​​对于两个事务 T1, T2。 T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了。

​幻读:​​事务T1读取一条指定where条件的语句,返回结果集。此时事务T2插入一些新记录,恰好满足T1的where条件。然后T1使用相同的条件再次查询,结果集中可以看到T2插入的记录,这些多出来的新纪录就是幻读。

或者说事务A开启事务,查询当前表;事务B开启事务插入一条新数据(id为主键);事务A 插入和事务B一样的数据,会出现锁等待超时现象。事务B提交后,事务A再次尝试插入,出现Duplicate entry…事务B很诧异,明明查询出来不存在将要插入的数据┭┮﹏┭┮

不可重复读重点是在update,即事务前后对比特定数据内容的修改。而幻读是insert和delete,即事务前后数据结果集的对比。


⑦ 事务隔离级别与存在问题

隔离级别

存在问题

READ UNCOMMITTED

脏读、不可重复读、幻读

READ COMMITTED

不可重复读、幻读

REPEATABLE READ

幻读

SERIALIZABLE


​Oracle​​​数据库支持​​READ COMMITTED(默认)​​​ 和 ​​SERIALIZABLE​​这两种事务隔离级别。所以Oracle不会出现脏读。

MySQL 支持 4 种事务隔离级别:​​READ_UNCOMMITTED​​​(读取未提交),​​READ_COMMITTED​​​(读取已提交),​​REPEATABLE_READ​​​(可重复读-默认)和​​SERIALIZABLE​​ (可串行化)。

SQL标准所定义的默认事务隔离级别是​​SERIALIZABLE​​​,但是​​Oracle​​​ 默认使用的是​​READ​​​ ​​COMMITTED​​​。MySQL默认事务隔离级别为​​REPEATABLE_READ​​。

事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持。需要注意的是事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。

⑧ 设置MySQL的事务隔离级别

每启动一个mysql 程序, 就会获得一个单独的数据库连接。每个数据库连接都有一个全局变量​​@@tx_isolation​​, 表示当前的事务隔离级别。

查看当前的隔离级别:

SELECT @@tx_isolation;

设置当前mySQL 连接的隔离级别:

set transaction isolation level read committed;

设置数据库系统的全局的隔离级别:

set global transaction isolation level read committed;

【4】事务的其他属性

① 事务的回滚属性

默认情况下只有未检查异常(​​RuntimeException和Error类型的异常​​)会导致事务回滚,而受检查异常不会。

事务的回滚规则可以通过 rollbackFor 和 noRollbackFor 属性来定义. 这两个属性被声明为 ​​Class[]​​ 类型的, 因此可以为这两个属性指定多个异常类.

rollbackFor:  遇到时必须进行回滚
noRollbackFor: 一组异常类,遇到时必须不回滚

rollbackFor={UserAccountException.class},
noRollbackFor={ArithMeticException.class}

② 事务的超时和只读属性

由于事务可以在行和表上获得锁, 因此长事务会占用资源, 并对整体性能产生影响。
如果一个事物只读取数据但不做修改, 数据库引擎可以对这个事务进行优化。

  • 超时事务属性: (​​timeout=millons​​ )事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
  • 只读事务属性:(readOnly=true/false) 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。

【5】MySQL如何避免脏读、不可重复读和幻读?

MySQL中默认使用REPEATABLE-READ的事务隔离级别,可以避免脏读,不可重复读但是仍然会出现幻读现象。确切的说,MySQL的InnoDB可以在一定程度上防止幻读,但是不能完全避免。

Oracle默认使用READ COMMITTED事务隔离级别,

By default, InnoDB operates in REPEATABLE READ transaction isolation
level and with the innodb_locks_unsafe_for_binlog system variable
disabled. In this case, InnoDB uses next-key locks for searches and
index scans, which prevents phantom rows (see Section 13.6.8.5,
“Avoiding the Phantom Problem Using Next-Key Locking”).

To prevent phantoms, InnoDB uses an algorithm called next-key
locking that combines index-row locking with gap locking.

You can use next-key locking to implement a uniqueness check in your
application: If you read your data in share mode and do not see a
duplicate for a row you are going to insert, then you can safely
insert your row and know that the next-key lock set on the successor
of your row during the read prevents anyone meanwhile inserting a
duplicate for your row. Thus, the next-key locking enables you to
“lock” the nonexistence of something in your table.

MySQL manual里对可重复读里的锁的详细解释:

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE 
MODE),UPDATE, and DELETE statements, locking depends on whether the
statement uses a unique index with a unique search condition, or a
range-type search condition. For a unique index with a unique search
condition, InnoDB locks only the index record found, not the gap
before it. For other search conditions, InnoDB locks the index range
scanned, using gap locks or next-key (gap plus index-record) locks
to block insertions by other sessions into the gaps covered by the
range.

使用如下命令查看隔离级别:

1.查看当前会话隔离级别

select @@tx_isolation;

2.查看系统当前隔离级别

select @@global.tx_isolation;

3.设置当前会话事务隔离级别

set session transaction isolation level read committed;

4.设置系统当前隔离级别

set global transaction isolation level repeatable read;

如下图所示:

MySQL中事务详解_mysql事务_02


第一个测试示例如下(不手动加锁):

① 事务A查询表中id为9的数据

MySQL中事务详解_mysql事务_03

没有没有还是没有!


② 事务B向表中插入id为9的数据,暂不提交

start TRANSACTION;

insert into t_user(id,name,age)VALUES(9,'jane1',18);

SELECT * from t_user;

没提交的数据在日志中,没有持久化到数据库!!


③ 事务A去尝试更新id为9的数据

mysql> update t_user set name='jane00'where id=9;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

锁等待超时,什么鬼,谁加锁了(事务B的insert 添加了排它锁)?


④ 事务B将事务提交,此时数据持久化到数据库

MySQL中事务详解_spring_04


⑤ 事务A再次尝试更新id为9的数据

mysql> update t_user set name='jane00'where id=9;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

发生了什么?怎么成功了,不是没有该条数据么?让我再次查一下看看:
MySQL中事务详解_spring_05

我擦我擦,见鬼了,凭空出现了数据!!!

update 时使用了当前读(读取最新数据),再次查询的时候会查询最新数据。


第二个测试实例如下(手动加共享锁):

① 事务A查询id为10的数据并使用共享锁

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_user where id =10 lock in share mode;
Empty set (0.00 sec)

② 事务B尝试插入id为10的数据

start TRANSACTION;

SELECT * from t_user;

insert into t_user(id,name,age)VALUES(10,'jane1',18);

被阻塞了,等待然后到来的是锁等待超时(事务A已经加了行级锁中的共享锁,事务B只能读,不能写):

Err] 1205 - Lock wait timeout exceeded; try restarting transaction

③ 事务A尝试更新id为10的数据

mysql> update t_user set name='janei' where id =10;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

# 很显然 空语句,那就提交吧。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

④ 事务B再次插入数据并提交

insert into t_user(id,name,age)VALUES(10,'jane1',18);

SELECT * from t_user;

COMMIT;

此时数据表中有了id为10的数据:
MySQL中事务详解_锁_06

唔,使用共享锁好像可以了,就是等待超时会抛异常。


第三个测试示例如下(使用排它锁/独占锁/互斥锁):

① 事务A查询当前表

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_user for update;
# 没有使用索引,将会加表锁

MySQL中事务详解_锁_07

唔,很好,没有id为11的数据。

② 事务B尝试插入id为11的数据,插入前先使用排它锁查一下吧

start TRANSACTION;

select * from t_user for update;

# 直接爆异常--表被事务加表锁了,不能再加其他锁
[SQL]select * from t_user for update;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

事务A不提交,事务B是没法执行的。使用共享锁和排它锁貌似挺好用的,应该没有其他问题了吧?


第四个测试示例如下(同样使用for update):

此时数据表数据如下:

MySQL中事务详解_spring_08
① 事务A对max(id)进行加锁

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select max(id) from t_user for update;
+---------+
| max(id) |
+---------+
| 10 |
+---------+
1 row in set (0.00 sec)

# [10,+∞)范围内的id都被加了间隙锁+X锁。

② 事务B尝试插入id 4 5 和20的 数据并提交事务。

start TRANSACTION;

insert into t_user(id,name,age)VALUES(4,'jane1',18);
insert into t_user(id,name,age)VALUES(5,'jane1',18);
insert into t_user(id,name,age)VALUES(20,'jane1',18);
COMMIT;

id为4和5数据插入正常,id为20数据插入失败。

[SQL]
insert into t_user(id,name,age)VALUES(20,'jane1',18);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

③ 事务A尝试更新id为4的记录并查询表记录数

mysql> update t_user set name='januie'where id=4;
Query OK, 1 row affected (42.56 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from t_user ;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | jane1 | 18 |
+----+--------+------+
8 rows in set (0.00 sec)

什么情况?事务A已经使用了for update,怎么事务B还能插进去?为什么插入 id 为4和 5 正常,插入id为20失败?

事务A不光update成功了,而且查询记录还多出来两条!!!


第五个测试示例(不加锁,事务A只做普通查询)

① 事务A查询表记录

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
8 rows in set (0.00 sec)

8条记录。

② 事务B插入id为6的记录并提交

start TRANSACTION;

insert into t_user(id,name,age)VALUES(6,'jane1',18);

SELECT * from t_user;

COMMIT;

此时数据库实际有9条数据。

③ 事务A再次查询数据表记录

mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
8 rows in set (0.00 sec)

嗯,很好的保证了可重复读,还是8条数据。

④ 事务A提交事务后再次查询表记录

mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 6 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
9 rows in set (0.00 sec)

(ÒωÓױ),数据库有9条啊,事务A刚才看的不是最新数据,是历史数据!!!


【6】快照读、当前读

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外)

select * from table where ?;

在事务中,执行普通select查询之后,会创建快照,后面再执行select语句时,查询的其实是前面生成的快照。这也就是MySQL的InnoDB默认事务隔离级别repeatable read可以有可重复读。

在事务中的读操作通过对当前的数据库中记录一个版本,以后的读操作只会读取记录的版本,因此相当于对数据库的数据建立了一个快照数据,因此叫做快照读,其不用对数据库中的数据进行加锁又叫做乐观锁。


当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

执行如下类似命令则会执行当前读,获取最新数据。

select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

这也就解释了为什么上面第一个测试示例中更新"不存在"的数据可以成功,并查询到了原先没有查询到的数据。


【7】MySQL的锁机制

MySQL的InnoDB锁机制分为表级锁和行级锁,官网文档:​​点击查看​

① 行级锁

行级锁中有共享锁和排它锁。

共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

select * from t_user where id =10 lock in share mode;

排他锁又称为写锁(独占锁),简称X锁,顾名思义,排他锁就是不能与其他所并存。

如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

select * from t_user where id =10 for update;

MySQL InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。

实例如下:

select * from t_user where id=10 for update; # 错误
select * from t_user where id=10 lock in share mode; # 错误
select * from t_user where id=1 # 正常获取数据

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。

意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

InnoDB行锁模式兼容性列表:

兼容性

X

IX

S

IS

X

冲突

冲突

冲突

冲突

IX

冲突

兼容

冲突

兼容

S

冲突

冲突

兼容

兼容

IS

兼容

兼容

兼容

兼容


② InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。

InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!


③ 间隙锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁。

对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。

InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!


总结:MySQL避免幻读是需要分情形的。

  • 在快照读读情况下,mysql通过mvcc来避免幻读。
  • 在当前读读情况下,mysql通过next-key来避免幻读

MVCC

mvcc全称是multi version concurrent control(多版本并发控制)。mysql把每个操作都定义成一个事务,每开启一个事务,系统的事务版本号自动递增。每行记录都有两个隐藏列:创建版本号和删除版本号

select:事务每次只能读到创建版本号小于等于此次系统版本号的记录,同时行的删除版本号不存在或者大于当前事务的版本号。

update:插入一条新记录,并把当前系统版本号作为行记录的版本号,同时保存当前系统版本号到原有的行作为删除版本号。

delete:把当前系统版本号作为行记录的删除版本号

insert:把当前系统版本号作为行记录的版本号


next-key锁

可以简单的理解为X锁+GAP锁。


【8】Spring事务与数据库事务及锁

Spring事务本质上使用数据库事务,而数据库事务本质上使用数据库锁,所以spring事务本质上使用数据库锁,开启spring事务意味着使用数据库锁。

① 那么事务的隔离级别与锁有什么关系呢?

事务的隔离级别是通过锁的机制实现的,事务的隔离级别是数据库开发商根据业务逻辑的实际需要定义的一组锁的使用策略。当我们将数据库的隔离级别定义为某一级别后如仍不能满足要求,我们可以自定义 sql 的锁来覆盖事务隔离级别默认的锁机制。

Spring事务实际使用AOP拦截注解方法,然后使用动态代理处理事务方法,捕获处理过程中的异常,spring事务其实是把异常交给spring处理。

Spring事务只有捕获到异常才会终止或回滚,如果你在程序中try/catch后自己处理异常而没有throw,那么事务将不会终止或回滚,失去事务本来的作用。

Spring事务会捕获所有的异常,但只会回滚数据库相关的操作,并且只有在声明了​​rollbackForClassName="Exception"​​之类的配置才会回滚。

Spring事务会回滚同一事务中的所有数据库操作,本质上是回滚同一数据库连接上的数据库操作;


② 对象锁和spring事务的对比

对象锁可以保证数据一致性和业务逻辑正确性,但不能保证并发性。

spring事务不能严格保证数据一致性和业务逻辑正确性,但具有较好的并发性,因为只锁数据库行数据。