前言

事务隔离性是由锁来实现的。原子性、一致性、持久性通过数据库的 redo log 和 undo log来完成。redo log 称为重做日志,用来保证事务的原子性和持久性。undo log 用来保证事务的一致性。redo 和 undo 的作用都可以视为一种恢复操作,redo 恢复提交事务修改的页操作,undo 回滚记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

重做日志用来实现事务的持久性,即事务ACID中的 D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log 和l undo log。redo log 用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。
为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次 fsync 操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盈的性能决定了事务提交的性能,也就是数据库的性能。

在 InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block〉的方式进行保存的,称之为重做日志块(redolog block),每块的大小为512字节。
若一个页中产生的重做日志数量大于512字节,那么需要分制为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写人可以保证原子性,不需要doublewrite技术。
重做日志块除了日志本身之外,还由日志块头(log block header)及日志块尾(logblock tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节。故每个重做日志块实际可以存储的大小为492字节(512-12-8)。

用户通常对undo有这样的误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子——但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了。

MySQL 事务特性(ACID)

  1. Atomicity : 原子性,原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  2. Consistency : 一致性,事务前后数据的完整性必须保持一致。
  3. Isolation : 隔离性,事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
  4. Durability : 持久性,持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

    MySQL 事务命令

    1. # 查询事务是否是自动提交
    2. select @@autocommit;
    3. # 设置事务不自动提交
    4. set autocommit=0;
    5. # 查看 MySQL 事务超时时间(默认50s)
    6. SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
    7. # 设置 MySQL 事务超时时间
    8. set innodb_lock_wait_timeout=100
    9. # 查看当前正在运行的事务
    10. SELECT * FROM information_schema.innodb_trx;
    11. # 查看系统级别的事务隔离级别
    12. select @@global.transaction_isolation;
    13. # 查看会话级别的事务隔离级别
    14. select @@transaction_isolation;

    查看当前正在运行的事务

    SELECT * FROM information_schema.innodb_trx;
    mysql> SELECT * FROM information_schema.innodb_trx;
    +--------+-----------+---------------------+-----------------------+------------------+------------+---------------------+-----------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+---------------------+
    | trx_id | trx_state | trx_started         | trx_requested_lock_id | trx_wait_started | trx_weight | trx_mysql_thread_id | trx_query | trx_operation_state | trx_tables_in_use | trx_tables_locked | trx_lock_structs | trx_lock_memory_bytes | trx_rows_locked | trx_rows_modified | trx_concurrency_tickets | trx_isolation_level | trx_unique_checks | trx_foreign_key_checks | trx_last_foreign_key_error | trx_adaptive_hash_latched | trx_adaptive_hash_timeout | trx_is_read_only | trx_autocommit_non_locking | trx_schedule_weight |
    +--------+-----------+---------------------+-----------------------+------------------+------------+---------------------+-----------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+---------------------+
    |   1744 | RUNNING   | 2021-11-15 15:07:39 | NULL                  | NULL             |         27 |                  66 | NULL      | NULL                |                 0 |                 1 |               27 |                  3488 |            4105 |                 0 |                       0 | REPEATABLE READ     |                 1 |                      1 | NULL                       |                         0 |                         0 |                0 |                          0 |                NULL |
    +--------+-----------+---------------------+-----------------------+------------------+------------+---------------------+-----------+---------------------+-------------------+-------------------+------------------+-----------------------+-----------------+-------------------+-------------------------+---------------------+-------------------+------------------------+----------------------------+---------------------------+---------------------------+------------------+----------------------------+---------------------+
    1 row in set (0.00 sec)
    
  • trx_id : 事务ID
  • trx_state : 事务状态
  • trx_started : 事务开始时间
  • trx_requested_lock_id : innodb_locks.lock_id
  • trx_wait_started : 事务开始等待的时间
  • trx_weight :
  • trx_mysql_thread_id : 事务线程ID
  • trx_query : 具体SQL语句
  • trx_operation_state : 事务当前操作状态
  • trx_tables_in_use : 事务中有多少个表被使用
  • trx_tables_locked : 事务拥有多少个锁
  • trx_lock_structs :
  • trx_lock_memory_bytes : 事务锁住的内存大小(B)
  • trx_rows_locked : 事务锁住的行数
  • trx_rows_modified : 事务更改的行数
  • trx_concurrency_tickets : 事务并发票数
  • trx_isolation_level : 事务隔离级别
  • trx_unique_checks : 是否唯一性检查
  • trx_foreign_key_checks : 是否外键检查
  • trx_last_foreign_key_error : 最后的外键错误
  • trx_adaptive_hash_latched :
  • trx_adaptive_hash_timeout :
  • trx_is_read_only :
  • trx_autocommit_non_locking :
  • trx_schedule_weight :

    MySQL 事务隔离级别

    MySQL 事务隔离级别默认:REPEATABLE READ,Oracle 事务隔离级别默认:READ COMMITTED

  • 官方事务隔离级别地址:https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html | Isolation Level | Dirty Read | Unrepeatable Read | Phantom Read | | —- | —- | —- | —- | | READ UNCOMMITTED | YES | YES | YES | | READ COMMITTED | NO | YES | YES | | REPEATABLE READ | NO | NO | YES | | SERIALIZABLE | NO | NO | NO |

  • MySQL 默认事务隔离级别是REPEATABLE READ,REPEATABLE READ 有一个特性是快照读,这就意味着同一个事务内,只做读取数据,无论读取多少次都是读第一次读取建立的快照数据。

  • MySQL 事务隔离级别是 READ COMMITTED, READ COMMITTED 有一个特性是当前读,这就意味着同一个事务内,无论读取多少次数据,都是去取数据库最新的数据。

    四个隔离级别说明

  1. 读未提交:一个事务还没有提交时,它所做的变更就能够被其他事务所看到;
  2. 读已提交:一个事务提交之后,它所做的变更才会被其他事务所看到;
  3. 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动是看到的数据是一致的。
  4. 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行

    Record Lock/Gap Lock/Next-Key Lock

MySQL InnoDB 支持三种锁定方式:

  1. Record Lock : 行锁,锁直接加在索引记录上面,锁住的是key。
  2. Gap Lock : 间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。
  3. Next-Key Lock : 行锁和间隙锁组合起来就叫Next-Key Lock。

MySQL 默认事务隔离级别是REPEATABLE READ,REPEATABLE READ事务隔离级别默认会有间隙锁和行锁,READ COMMITTED 事务隔离级别默认会有行锁。

for update 排它锁

InnoDB 默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别。
使用方式

# 表级锁
select * from TABLE_NAME for update;
# 行级锁
select * from TABLE_NAME where id=1 for update;

# for update 会让其他事务等待
# for update nowait 会直接拒绝其他事务
select * from TABLE_NAME where id=1 for update nowait;

当一个方法调用 for update 语句时,其他方法再次调用会出现 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 异常。
image.png
怎么校验 for update 是表级锁还是行级锁,有一个特征是,表级锁锁的是整张表,行级锁锁的是一行数据,如果是表级锁的话,整个表都无法进行CRUD的操作,如果是行级锁,只有那一行数据无法进行CRUD操作,其他行数据依然可以进行CRUD操作。或者通过 SELECT * FROM information_schema.innodb_trx; 查看事务锁定的行数,来判定表级锁还是行级锁;

# 明确指定主键,行级锁
select * from city where ID=4083 for update;
# 明确指定主键,也有其他条件,行级锁
select * from city where District='Kabol----' and ID=4083 for update;

# 没有主键,表级锁
select * from city where District='Kabol' for update;

lock in share mode 共享锁

共享锁即在符合条件的rows上都加了共享锁。如果事务对某行数据加上共享锁之后,可进行读写操作;其他事务可以对该数据加共享锁,但不能加排他锁,且只能读数据,不能修改数据。 某个事物想进行修改数据操作,那他必须等其他事物的共享锁都释放完毕才能进行修改操作。

# 添加供应商锁的方式
select * from city where ID=4083 lock in share mode;

image.png