好文推荐:专题

事务

特性

我们常说的事务具有ACID的特性

  • 原子性、一致性 主要由日志来保证
  • 隔离性就需要锁了
  • A:
    • Atomicity(原子性)
    • 一个事务中的那组SQL语句,要么全部执行成功,要么一起失败。如果其中有一条SQL语句执行发生错误,就会回滚。回到最初的状态。不会出现,有的SQL执行成功,有的SQL执行失败。
    • 一荣俱荣、一损俱损
  • C:
    • Consistency(一致性)
    • 执行的中间状态不可见,只会存在执行前的状态和执行后的状态。也就是说看见的数据是执行前的数据或者执行后的数据,保证了数据的一致性
  • I:
    • Isolation(隔离性)
    • 并发事务时,当多个事务对相同的数据进行修改时,隔离性保证每个事务隔离不会影响其他事务,防止事务的交叉执行对数据产生预期之外的结果
  • D:
    • Durablity(持久性)
    • 最终执行的结果将数据写入磁盘,保证数据不丢失

执行过程

• MYISAM 下的提交时自动开启的

• INNODB 下默认也是自动开启的,但是开启事务的话,自动提交不会默认提交

• 查看当前自动提交是否开启:SHOW VARIABLES LIKE ‘autocommit’;

• 设置自动提交开启/关闭:

• ON:set autocommit = 1;

• OFF:set autocommit = 0;

使用保存点

使用情形

假如我有三个用户A、B、C,A分别向B和C各子装入100,可能A向B转账成功,但是A向C转账失败,我现在不想要因为向其中某一人转账失败,就所有的转账都回滚

… ….

使用 savepoint pointname 来设置一个保存点;

使用 rollback to **pointname 来会滚到这个保存点;**

使用案例

  1. -- A 分别向 B C 各自转账 100, 先查询出三人都是500
  2. mysql> select * from account
  3. +----------------+---------+
  4. | account_number | balance |
  5. +----------------+---------+
  6. | A | 500 |
  7. | B | 500 |
  8. | C | 500 |
  9. +----------------+---------+
  10. -- 开启事务
  11. mysql> begin;
  12. -- AB转账100
  13. update account set balance = balance - 100 where account_number = 'A';
  14. update account set balance = balance + 100 where account_number = 'B';
  15. -- 设置一个保存点
  16. SAVEPOINT transfer_toB;
  17. -- AC转账100
  18. update account set balance = balance - 100 where account_number = 'A';
  19. update account set balance = balance + 100 where account_number = 'C';
  20. -- 查询此时的情况,我此时没有提交
  21. mysql> select * from account
  22. +----------------+---------+
  23. | account_number | balance |
  24. +----------------+---------+
  25. | A | 300 |
  26. | B | 600 |
  27. | C | 600 |
  28. +----------------+---------+
  29. -- 此时我如果发生了异常,情况AC转账发生了异常
  30. -- 回到保存点
  31. ROLLBACK to transfer_toB;
  32. commit;
  33. -- 再去查看
  34. mysql> select * from account
  35. +----------------+---------+
  36. | account_number | balance |
  37. +----------------+---------+
  38. | A | 300 |
  39. | B | 600 |
  40. | C | 600 |
  41. +----------------+---------+

事务隔离级别

介绍

当多个事务同时发生时,事务的隔离级别就确定了事务间对相同资源的读取和写入问题

以下是mysql8的事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read) 否(这里做了更改
串行化(serializable)

分类

  1. 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  2. 读已提交(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  3. 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读
  4. 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

事务并发引起的问题

  • 脏读:事务A读取了事务B未提交的数据,事务B回滚,事务A则读取的数据是脏数据
  • 不可重复读:事务A在多次读取同一批数据时,其它事务对数据进行了修改,导致事务A读取的数据不一致
    • 解决办法,锁住读取的数据
  • 幻读:事务A在读取数据时,其他事务增加或者修改了数据
    • 解决办法,锁表
    • mysql,已经解决了幻读的问题

验证 & 详细解读

RU 读未提交隔离级别(read uncommitted)

时间线 事务A 事务B
1 设置当前会话的事务隔离级别为 读未提交 设置当前会话的事务隔离级别为 读未提交
set transaction_isolation = ‘read-uncommitted’;
select @@transaction_isolation;
set transaction_isolation = ‘read-uncommitted’;
select @@transaction_isolation;
mysql - 图1 mysql - 图2
2 开启事务A,并查询当前account表的数据 开启事务B,并查询当前account表的数据
begin;
select * from account;
begin;
select * from account;
mysql - 图3 mysql - 图4
3 修改账户A的金额并进行查询
update account set balance = 100 where account_number = ‘A’;
select * from account;
mysql - 图5
4 此时事务A并没有提交数据,我们在事务B中查询account表的情况看看
select * from account
mysql - 图6
读未提交:
+ 我们从结果中可以看到,事务B可以读取到事务A中未提交的数据。
+ 这引发了一个问题,那就是脏读。试想一下如果事务A因为某些原因回滚,如果事务B在事务A修改后回滚前读
读取了数据,那么读取的这条数据就是错误的数据
+ 这是事务隔离级别中最低一级的隔离级别,我们一般几乎是不会遇到

RC 读提交隔离级别 (read committed)

线 事务A 事务B
1 设置当前会话的事务隔离级别为 读已提交 设置当前会话的事务隔离级别为 读已提交
set transaction_isolation = ‘read-committed’;
select @@transaction_isolation;
set transaction_isolation = ‘read-committed’;
select @@transaction_isolation;
mysql - 图7 mysql - 图8
2 开启事务A,并查询当前account表的数据 开启事务B,并查询当前account表的数据
begin;
select * from account;
begin;
select * from account;
mysql - 图9 mysql - 图10
3 修改账户A的金额并进行查询
update account set balance = 200 where account_number = ‘A’;
select * from account;
mysql - 图11
4 此时事务A并没有提交数据,我们在事务B中查询account表的情况看看
select * from account
mysql - 图12
可以看到这里 事务B读取到的还是事务A的未提交之前的记录。
5 事务A提交
commit;
6 此时,再去事务B中查看account表
select * from account
mysql - 图13
读已提交:
+ 出现一个问题,那就是如果最后事务A成功提交。那么事务B两次读的结果不一样。
+ 所以读已提交也会造成问题,那就是一个事务前后读取到的数据不一致问题。
+ 造成了不可重复读的问题
+ 这种隔离级别已经是大多数数据库的隔离级别(Oracle的隔离级别),但不是mysql数据库的隔离级别。

RR 可重复读隔离级别(Repeatable read)

时间线 事务A 事务B
1 设置当前会话的事务隔离级别为 可重复读 设置当前会话的事务隔离级别为 可重复读
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
2 开启事务A,并查询当前account表的数据 开启事务B,并查询当前account表的数据
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 300
B 500
+————————+————-+
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 300
B 500
+————————+————-+
3 修改账户A的金额并进行查询
update account set balance = 400 where account_number = ‘A’;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 400
B 500
+————————+————-+
4 此时A未提交,在事务B中查看account表
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 300
B 500
+————————+————-+
2 rows in set (0.00 sec)
5 事务A提交
commit;
6 此时,再去事务B中查看account表
select * from account
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 300
B 500
+————————+————-+
2 rows in set (0.00 sec)
可以看到无论A是否提交,在事务B中看到的都是一致的数据,这个数据和一开始A未做修改的数据一致
7 此时我去提交B,然后再看
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 400
B 500
+————————+————-+
可重复读:
+ 我们可以看到事务B,查询的结果,不会受到事务A的影响。保证了数据的一致性。
+ 可重复读的隔离级别下是用来 MVCC机制,select 操作不会更新版本号是快照读(历史版本);
insert、update和delete会更新版本号,是当前读(当前版本)。
+ 另外mysql8开始,默认的可重复读下解决了幻读的问题,此时如果在A中插入或删除数据,会报错
lock wait timeout …. 进行锁等待

innoDB 解决了幻读

前面的定义中RR级别是可能产生幻读,这是在传统的RR级别定义中会出现的。但是在innoDB引擎中利用MVCC多版本并发控制解决了这个问题

mysql - 图14

这算是幻读吗?在标准的RR隔离级别定义中是无法解决幻读问题的,比如我要保证可重复读,那么我们可以在我们的结果集的范围加一个锁(between 1 and 11),防止数据更改.但是我们毕竟不是锁住真个表,所以insert数据我们并不能保证他不插入。所以是有幻读的问题存在的。但是innodb引擎解决了幻读的问题,基于MVCC(多版本并发控制):在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。所以当我们执行update的时候,当前事务的版本号已经更新了?所以也算是幻读??(存疑)主要是gap间隙锁+MVCC解决幻读问题?

Mysql INNODB中的 可重复读

  • 一个事务通过一条语句只能看到相同的数据,即使在另外一个事务中对数据进行了修改。
  • 假设,开启两个事务A、B。B事务的提交前和提交后A事务读取的是一样的数据,这个数据是A未做修改的数据。只有一个例外就是:事务可以读取自己在同一事务中更改的数据。
  • 事务A开始第一次读取数据。会创建读取视图并保持打开状态,知道事务A结束。为了在结束前都提供相同的结果集,INNODB 使用了 MVCC(多版本并发控制)和重做日志。假设事务A读取了几行的数据,另外一个事务B,恰好删除了这几行的数据。但是事务A并没有关闭,还是处于打开的状态,只要它还没提交,它还是可以看到这几行数据。这是为啥?因为已经删除的数据保留在UNDO日志空间中来继续履行事务A,但是只要事务A提交,也就是事务A完成,这些被标记的数据就会从重做日志中删除,这就是MVCC的体现。

存在的问题?

在RR模型,我们虽然避免了幻读,但是存在一个问题

我们得到的数据不是数据库中实时的数据,如果是对实时数据比较敏感的业务,这是不现实的。

  • 对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),
  • 而读取数据库当前版本数据的方式,叫当前读 (current read)。

很显然,在MVCC中:

  • 快照读:就是select : select * from table ….;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert;
  • update ;
  • delete;

事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。

比如,我们有以下的订单业务场景,我们队一个商品下单的操作,我们得首先检查这个订单的数量还剩多少,然后下单。

  1. -- 事务1
  2. select num from t_goods where id=1;
  3. update t_goods set num=num-$mynum where id=1;
  4. -- 事务2
  5. select num from t_goods where id=1;
  6. update t_goods set num=num-$mynum where id=1;

1.假设这个时候数量只有1,我们下单也是只有1.如果在并发的情况下,事务1查询到还有一单准备下单,但是这个时候事务2已经提交了。订单变成0.这个事务1在执行update,就会造成事故。

解决方法

2.解决问题方法1(悲观锁):就是利用for update对着个商品加锁,事务完成之后释放锁。切记where条件的有索引,否则会锁全表。

解决方法2(乐观锁):给数据库表加上个version字段。然后SQL改写:

  1. select num,version from t_goods where id=1;
  2. update t_goods set num=num-1,version=verison+1 where id=1 and version=${version}

Serializable 串行化隔离级别

时间线 事务A 事务B
1 设置当前会话的事务隔离级别为 序列化 设置当前会话的事务隔离级别为 序列化
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@transaction_isolation;
+————————————-+
@@transaction_isolation
+————————————-+
SERIALIZABLE
+————————————-+
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@transaction_isolation;
+————————————-+
@@transaction_isolation
+————————————-+
SERIALIZABLE
+————————————-+
2 开启事务A,并查询当前account表的数据 开启事务B,并查询当前account表的数据
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 400
B 500
+————————+————-+
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 400
B 500
+————————+————-+
3 在事务A中修改A账户的金额
mysql> update account set balance = 500 where account_number = ‘A’;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
4 事务A提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
5
再去修改
mysql> update account set balance = 500 where account_number = ‘A’;

mysql> select * from account;
+————————+————-+
account_number balance
+————————+————-+
A 500
B 500
+————————+————-+

Spring事务

管理事务的方式

编程式事务

编程式事务就是利用手动代码编写事务相关的业务逻辑,这种方式比较复杂、啰嗦,但是更加灵活可控制

  1. public void testTransactionTemplate() {
  2. TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);
  3. transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); //设置事务隔离级别
  4. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置为required传播级别
  5. ....
  6. transactionTemplate.execute(new TransactionCallbackWithoutResult() {
  7. @Override
  8. protected void doInTransactionWithoutResult(TransactionStatus status) { //事务块
  9. jdbcTemplate.update(INSERT_SQL, "test");
  10. }});
  11. }

声明式事务

1.xml:

为了避免我们每次都手动写代码,利用Spring AOP的方式对每个方法代理环绕,利用xml配置避免了写代码。

  1. <tx:advice id="txAdvice" transaction-manager="txManager">
  2. <tx:attributes> <!--设置所有匹配的方法,然后设置传播级别和事务隔离-->
  3. <tx:method name="save*" propagation="REQUIRED" />
  4. <tx:method name="add*" propagation="REQUIRED" />
  5. <tx:method name="create*" propagation="REQUIRED" />
  6. <tx:method name="insert*" propagation="REQUIRED" />
  7. <tx:method name="update*" propagation="REQUIRED" />
  8. <tx:method name="merge*" propagation="REQUIRED" />
  9. <tx:method name="del*" propagation="REQUIRED" />
  10. <tx:method name="remove*" propagation="REQUIRED" />
  11. <tx:method name="put*" propagation="REQUIRED" />
  12. <tx:method name="get*" propagation="SUPPORTS" read-only="true" />
  13. <tx:method name="count*" propagation="SUPPORTS" read-only="true" />
  14. <tx:method name="find*" propagation="SUPPORTS" read-only="true" />
  15. <tx:method name="list*" propagation="SUPPORTS" read-only="true" />
  16. <tx:method name="*" propagation="SUPPORTS" read-only="true" />
  17. </tx:attributes>
  18. </tx:advice>
  19. <aop:config>
  20. <aop:pointcut id="txPointcut" expression="execution(* org.transaction..service.*.*(..))" />
  21. <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
  22. </aop:config>

2.注解

  1. <tx:annotation-driven transaction-manager="transactioManager" /><!--开启注解的方式-->
  2. @Target({ElementType.METHOD, ElementType.TYPE})
  3. @Retention(RetentionPolicy.RUNTIME)
  4. @Inherited
  5. @Documented
  6. public @interface Transactional {
  7. @AliasFor("transactionManager")
  8. String value() default "";
  9. @AliasFor("value")
  10. String transactionManager() default "";
  11. Propagation propagation() default Propagation.REQUIRED;//传播级别
  12. Isolation isolation() default Isolation.DEFAULT;//事务隔离级别
  13. int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;//事务超时时间
  14. boolean readOnly() default false;//只读事务
  15. Class<? extends Throwable>[] rollbackFor() default {};//抛出哪些异常 会执行回滚
  16. String[] rollbackForClassName() default {};
  17. Class<? extends Throwable>[] noRollbackFor() default {};
  18. String[] noRollbackForClassName() default {};//不回滚的异常名称
  19. }
  20. //transaction注解可以放在方法上或者类上

事务传播

Spring管理的事务是逻辑事务,而且物理事务和逻辑事务最大差别就在于事务传播行为,事务传播行为用于指定在多个事务方法间调用时,事务是如何在这些方法间传播的,Spring共支持7种传播行为 为了演示事务传播行为,我们新建一张用户表

  1. EATE TABLE user (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `username` varchar(255) NOT NULL,
  4. `pwd` varchar(255) NOT NULL,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0;

Required:

必须有逻辑事务,否则新建一个事务,使用PROPAGATION_REQUIRED指定,表示如果当前存在一个逻辑事务,则加入该逻辑事务,否则将新建一个逻辑事务,如下图所示;

mysql - 图15

测试的代码如下,在account插入的地方主动回滚

  1. public int insertAccount(final String customer, final int money) {
  2. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置为required传播级别
  3. int re= transactionTemplate.execute(new TransactionCallback<Integer>() {
  4. public Integer doInTransaction( TransactionStatus status) {
  5. int i = accountDao.insertAccount(customer, money);
  6. status.setRollbackOnly();//主动回滚
  7. return i;
  8. }
  9. });
  10. return re;
  11. }
  12. public int inertUser(final String username, final String password) {
  13. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置为required传播级别
  14. transactionTemplate.execute(new TransactionCallbackWithoutResult() {
  15. @Override
  16. protected void doInTransactionWithoutResult(TransactionStatus status) {
  17. int i = userDao.inertUser(username, password);
  18. int hahha = accountService.insertAccount("hahha", 2222);
  19. // status.setRollbackOnly();
  20. System.out.println("user==="+i);
  21. System.out.println("account===="+hahha);
  22. }
  23. });
  24. return 0;
  25. }

按照required的逻辑,代码执行的逻辑如下:

在调用userService对象的insert方法时,此方法用的是Required传播行为且此时Spring事务管理器发现还没开启逻辑事务,因此Spring管理器觉得开启逻辑事务

在此逻辑事务中调用了accountService对象的insert方法,而在insert方法中发现同样用的是Required传播行为,因此直接使用该已经存在的逻辑事务;

返回userService,执行完并关闭事务

所以在这种情况下,两个事务属于同一个事务,一个回滚则两个任务都回滚。

RequiresNew:

创建新的逻辑事务,使用PROPAGATION_REQUIRES_NEW指定,表示每次都创建新的逻辑事务(物理事务也是不同的)如下图所示:

mysql - 图16

Supports:

支持当前事务,使用PROPAGATION_SUPPORTS指定,指如果当前存在逻辑事务,就加入到该逻辑事务,如果当前没有逻辑事务,就以非事务方式执行,如下图所示:

mysql - 图17

NotSupported:

不支持事务,如果当前存在事务则暂停该事务,使用PROPAGATION_NOT_SUPPORTED指定,即以非事务方式执行,如果当前存在逻辑事务,就把当前事务暂停,以非事务方式执行。

Mandatory:

必须有事务,否则抛出异常,使用PROPAGATION_MANDATORY指定,使用当前事务执行,如果当前没有事务,则抛出异常(IllegalTransactionStateException)。当运行在存在逻辑事务中则以当前事务运行,如果没有运行在事务中,则抛出异常

mysql - 图18

Never

不支持事务,如果当前存在是事务则抛出异常,使用PROPAGATION_NEVER指定,即以非事务方式执行,如果当前存在事务,则抛出异常(IllegalTransactionStateException)

mysql - 图19

Nested:

嵌套事务支持,使用PROPAGATION_NESTED指定,如果当前存在事务,则在嵌套事务内执行,如果当前不存在事务,则创建一个新的事务,嵌套事务使用数据库中的保存点来实现,即嵌套事务回滚不影响外部事务,但外部事务回滚将导致嵌套事务回滚。

mysql - 图20

Nested和RequiresNew的区别:

  1. RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务;
  2. Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而 RequiresNew由于都是全新的事务,所以之间是无关联的;
  3. Nested使用JDBC 3的保存点(save point)实现,即如果使用低版本驱动将导致不支持嵌套事务。

使用嵌套事务,必须确保具体事务管理器实现的nestedTransactionAllowed属性为true,否则不支持嵌套事务,如DataSourceTransactionManager默认支持,而HibernateTransactionManager默认不支持,需要设置来开启。

锁的简单介绍

  • 外部锁:MySQL为客户端会话可以 显示的锁表和解锁
    • 锁表:LOCK TABLES table_name [READ | WRITE]
      • READ:共享锁,不允许写入数据,可以读数据
      • WRITE:排它锁,除所释放外,其他会话读写都不允许
    • 解锁表:UNLOCK TABLES
    • 冻结所有表的写入:FLUSH TABLES WITH READ LOCK;
  • 内部锁:自身对资源的管理,管理会话对表数据的竞争
    • 行级别的锁:
      • 只有被访问的行会被锁定,允许多个会话同时进行访问,并发效果好。只有INNODB支持行级 别的锁。
    • 页级别的锁:
      • BDB存储引擎支持的锁,锁住一页数据大概是16KB,不过这种存储引擎几乎现在不会用到了,一般我们只会选择MYISAM和INNODB。
    • 表级别的锁:
      • 对于MYISAM、MERGE、MEMORY 存储引擎使用的锁

快照读(snapshot read)

  • 语句:
    • 简单的select语句(不包括SELECT … LOCK IN SHARE MODE 和 SELECT …FOR UPDATE
  • 实现:
    • 基于MVCC和 UNDO LOG 来实现
    • 而MySQL中 MVCC的实现又是依靠 ReadView(事务视图)来实现的。多个ReadView组成 回滚日志。每个普通的 select … from 查询的都是最新的 ReadView 的该条数据的值。
  • ReadView:ReadView是针对同一条数据生成的视图
    • 读已提交:
      • 事务中每个sql语句执行都会生成一个ReadView,所以一个事务中的多条sql语句也就生成了多个ReadView,而每条sql执行时,都会查询最新的那个ReadView的值。
      • 事务A有2个sql语句,第一个查询的sql生成了一个 id = n 的 ReadView。如果事务B对该数据进行了操作,那么就会生成 id = n+1 的 readview ,下一次查询就会选择 id = n+1 的 readview。
    • 可重复读:
      • 在事务开始的时候就会生成一个 readview ,所以同一个事务内的sql语句查询同一条数据时,每次读取到的都是同一个 readview,也就是这样保证了数据的一致性。
      • 举个栗子说明:
        • 当前事务A读取id=n的某一条记录,无论其他事务对其这条数据做任何修改,事务A未提交前,他读取的永远是没做任何修改的那条记录。

当前读(current read)

  • 语句:
    • select … lock in share mode
    • select … for update
    • insert
    • update
    • delete
  • 实现:
    • 是基于 临键锁(行锁 + 间歇锁)来实现的。
    • 更新数据时,都是先读后写,而这个读就是当前读。说白了,当前读就是携带锁的读,目的就是为了防止读数据时,其他事务对数据进行了修改,就是为了保证数据的安全性。

INNODB 默认情况下的加锁情况

在默认情况下(不去修改事务的隔离级别为 serializable),所有的 select … …. from 这样的SQL语句都是快照读,读取的是数据库的一个快照。是不会加任何锁的

serializable下,凡是通过beginl;start transaction 这样的语句开启了事务,就算是普通的select 语句,也会被隐式转换成 SELECT … LOCK IN SHARE MODE,所以还是加上了共享锁

而针对于写的操作,update、insert into、delete 这样的SQL,会自动加上排他锁(行级别的),为这些行加上了行级别的排他锁,所以也是避免了数据的不安全性。

原本是共享表空间,默认Innodb_file_per_table 参数开启,为每个表创建单独的表空间,即ibd文件。

保存了undo log、InnoDB表的元数据、Buffer。

MVCC实现

参考

1)Mutli-Version Concurreny Control,多版本并发控制,读不加锁,读写不冲突。应用于 Read Commited 和Repeatable read 两个事务隔离级别。

快照读: 普通的select,不加锁,读取记录的可见版本。

当前读:select...lock in share mode/for update、update、insert、delete,读取记录最新版本,并加锁。

ibd文件(又叫表空间)中包含数据段,索引段,回滚段(undo log)

回滚段中存储undo log,所谓的多版本数据指的是undo log中的多条记

2)数据行结构

mysql - 图21

DB_TRX_ID: 这行数据最后插入或修改的事务idDB_ROLL_PTR:当该行数据被更新的时候,修改前的内容记录到undo log中,回滚指针指向该条undo log记录DB_ROW_ID:表中不存主键 或者 唯一索引,那么数据库 就会采用DB_ROW_ID生成聚簇索引。

3)undo log

作用:undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。

存储位置:所有的undo log均存放在主ibd数据文件(表空间)中

创建时间:每次对数据进行更新操作时,copy 当前数据,保存到undo log 中,并将当前行的回滚指针指向undo log中的旧记录

mysql - 图22

4)实例

1)初始行

mysql - 图23

2)事务1更新操作(因为update会加排他锁X,所以undo log都是排队顺序的,不会出现两个事务并发增加undo log)

mysql - 图24

3)事务2更新操作

mysql - 图25

InnoDB 中的三种行锁(记录锁、间隙锁、临键锁)

参考

行锁在InnoDB中通过索引实现,所以一旦某个加锁的操作没有使用索引,那么就会由行锁升级成为表锁。

记录锁(Record Locks)

就是给某行的记录加锁,它是基于一行或者说是表中的一条记录,那么就一定需要依赖主键或者唯一索引,来进行精准匹配, 如:

  1. SELECT * FROM `STUDENT` WHERE `ID` = 1 FOR UPDATE;

注意ID这个字段必须为主键或者是唯一索引,否则加的锁就会变成临键锁。而且查询语句必须为精准匹配(=),像是>、<、like等也会退化成临键锁。

我们都知道 INNODB 在写的情况下会自动加上排他锁来保证数据的安全性。为了确保这个锁是仅仅针对于某一行记录的锁,我们通常这样来写

  1. -- id 是主键或者唯一索引
  2. UPDATE SET AGE = 50 WHERE ID = 1;

间隙锁(Gap Locks)

间隙锁基于非唯一索引,它锁定的是一段范围性的索引记录。间隙锁基于 Next-Key Locking 算法,使用间隙锁锁住的是一个区间 ,而不仅仅是这个区间中的每一条数据。如:

  1. SELECT * FROM table WHERE ID BETWEEN 1 AND 10 FOR UPDATE;

所有在(1, 10)区间内的记录行都会被锁住。除了像是这样手动加上一个间隙锁在执行完某些 SQL 后,InnoDB 也会自动加间隙锁


临键锁(Next-Key Locks)

临键锁可以理解为一种特殊的间隙锁,它是一种特殊的算法。通过临键锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁。当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。

也就是(a, b]. InnoDB中行级锁是基于索引实现的,临建锁只与非唯一索引列有关,所以在唯一索引列上(包括主键)不存在临建锁

  1. -- 开启一个cmd
  2. mysql> use cook;
  3. Database changed
  4. mysql> select version();
  5. +-----------+
  6. | version() |
  7. +-----------+
  8. | 8.0.12 |
  9. +-----------+
  10. 1 row in set (0.00 sec)
  11. mysql> select * from person;
  12. +----+-----+-------+
  13. | id | age | name |
  14. +----+-----+-------+
  15. | 1 | 10 | test1 |
  16. | 2 | 24 | test2 |
  17. | 3 | 32 | test3 |
  18. | 4 | 45 | test4 |
  19. +----+-----+-------+
  20. -- id 是主键,age name是普通字段
  21. -- 现在分析age可能存在的临键锁区间
  22. (-∞, 10],
  23. (10, 24],
  24. (24, 32],
  25. (32, 45],
  26. (45, +∞],
  27. -- 现在开启事务A
  28. mysql> begin;
  29. mysql> update person set name = 'xs' where age = 24;
  30. Query OK, 1 row affected (0.00 sec)
  31. -- 然后开启事务B
  32. mysql> begin;
  33. Query OK, 0 rows affected (0.01 sec)
  34. mysql> insert into person (id, age, name) values(5, 11, 'test5');
  35. ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  36. -- 我们可以看到,事务B的这条sql被阻塞了。
  37. -- 因为事务A在执行上述sql 后会获得一个(10, 32) 的临届主键。
  38. -- 哪怕age的范围也不行,因为... ...
  39. insert into person (id, age, name) VALUES (10, 44, 'fly');

小结

  • InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  • 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  • 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
  • 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。

锁队列

除了共享锁之外,没有任何两个锁可以一起加在同一个表上。如果有一个表加了一个共享锁,此时一个排他锁要进入,那么它将会被留在队列中,直到共享锁被释放。

InnoDB从表中读取/写入数据时会获取元数据锁,如果此时有一个事务请求 WRITE LOCK,则这个事务会被保留在队列,直到第一个事务完成。依次类推… …

  1. -- 锁队列测试
  2. -- 事务1 开启查询account表数据
  3. begin;
  4. select * from account;
  5. -- 查询的结果:
  6. account_number balance
  7. A 500
  8. B 500
  9. -- 注意我没有提交,事务1还是开启的状态
  10. -- 事务2 开启,锁表使用排他锁
  11. begin;
  12. LOCK TABLE account WRITE;
  13. -- 执行的结果:
  14. [SQL]LOCK TABLE account WRITE;
  15. -- 事务3 开启,查数据
  16. begin;
  17. select * from account;
  18. -- 执行的结果:
  19. [SQL]select * from account;
  20. -- 这里可以看到事务3什么也查询不到,它一直是阻塞的状态。
  21. -- 事务3在等待事务2的完成
  22. -- 事务2在等待事务1的完成
  23. -- 我们再来一个会话,来查看现在的状态
  24. SHOW PROCESSLIST;
  25. -- 执行结果
  26. id User Host DB COmmand Time State Info
  27. 4 event_scheduler localhost Daemon 917 Waiting on empty queue
  28. 8 root localhost:54688 cook Sleep 821
  29. 9 root localhost:54694 cook Query 731 Waiting for table metadata lock LOCK TABLE account WRITE
  30. 10 root localhost:54700 cook Sleep 715
  31. 11 root localhost:54705 cook Query 656 Waiting for table metadata lock select * from account
  32. 12 root localhost:54793 cook Sleep 46
  33. 13 root localhost:54800 cook Query 0 starting SHOW PROCESSLIST
  34. -- 可以直接从stateInfo看到事务2和事务3都在等待事务1
  35. -- 现在我提交事务1, 然后事务3他还是看不到,因为它在等待事务2
  36. commit;
  37. -- 继续提交事务2 然而还是不行??????
  38. commit;
  39. -- 因为
  40. Waiting for table metadata lock
  41. 场景一:长事物运行,阻塞DDL,继而阻塞所有同表的后续操作
  42. 场景二:未提交事物,阻塞DDL,继而阻塞所有同表的后续操作
  43. 场景三:
  44. 通过show processlist看不到TableA上有任何操作.
  45. information_schema.innodb_trx中也没有任何进行中的事务。
  46. 这很可能是因为在一个显式的事务中,对TableA进行了一个失败的操作(比如查询了一个不存在的字段),
  47. 这时事务没有开始,但是失败语句获取到的锁依然有效,没有释放。
  48. performance_schema.events_statements_current 表中可以查到失败的语句。
  49. 官方手册上对此的说明如下:
  50. If the server acquires metadata locks for a statement that is syntactically
  51. valid but fails during execution, it does not release the locks early.
  52. Lock release is still deferred to the end of the transaction
  53. because the failed statement is written to the binary log and
  54. the locks protect log consistency.
  55. 也就是说除了语法错误,其他错误语句获取到的锁在这个事务提交或回滚之前,仍然不会释放掉。
  56. because the failed statement is written to the binary log and
  57. the locks protect log consistency
  58. 但是解释这一行为的原因很难理解,因为错误的语句根本不会被记录到二进制日志。
  59. 处理方法:通过performance_schema.events_statements_current找到其sid,
  60. kill 掉该session.
  61. 也可以 kill DDL所在的session.
  62. -- 事务的长时间运行和阻塞都会造成很严重的问题。
  63. -- 另外为了保证备份的一致性,所有的备份方法都会使用
  64. FLUSH TABLES WITH READ LOCK READ;
  65. -- 如果此时表中存在长时间运行的事务 .......
  1. -- 共享锁测试:
  2. -- 事务1加上一把共享锁
  3. begin;
  4. select * from account where account_number = 'A' lock in share mode ;
  5. -- 事务2
  6. begin;
  7. select * from account where account_number = 'A';
  8. -- 结果:事务2中是可以查询出这条记录的

参考:https://www.jianshu.com/p/de595d66f6aa

https://www.yuque.com/jiurenbufushizhibuyu/yb3lga/fhpmo0

视频教程:https://www.bilibili.com/video/BV12b411K7Zu?p=178

mybatis

#使用预编译,$使用拼接SQL。

mysql注入审计