1.MySQL锁概述

  • MyISAM存储引擎采用的是表级锁
  • InnoDB存储引擎既支持行级锁,也支持表级锁,默认情况下是采用行级锁
  • MySQL锁的特性:
  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
  • 页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

2.InnoDB锁问题

  • InnoDB与MyISAM的最大不同有两点:支持事务(TRANSACTION);采用了行级锁。

事务(Transaction)及其ACID属性

  • 事务是由一组SQL语句组成的逻辑处理单元,具有以下4个属性:
  • 原子性Atomicity,或称不可分割性):事务是一个原子单元,其对数据的修改,要么全都执行,要么全都不执行。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样;
  • 一致性Consistency):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事物的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的;(总是从一个一致性的状态转换到另一个一致性的状态)
  • 隔离性Isolation,又称独立性):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的;(多个事务并发执行时,一个事务的执行不影响其他事务的执行)
  • 读未提交(Read uncommitted)
  • 读提交(read committed)
  • 可重复读(repeatable read)
  • 可序列化(Serializable)
  • 持久性Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失;

并发事务处理带来的问题

  • 相对于串行处理,并发事务处理能力大大增加数据库资源的利用率,提高数据库的事务吞吐量,可以支持更多用户,但也会带来一些问题;
  • 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了由其他事务所做的更新;(如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免子问题)
  • 脏读(Dirty Read):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步处理,就会产生未提交的数据依赖关系;
  • 不可重复读(Non-Repeatable Read):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变或某些记录已经被删除了;
  • 幻读(Phantom Read):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。

事务隔离级别

  • 防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决
  • “脏读” ”不可重复“和”幻读“,作为数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
  • 数据库实现事务隔离的方式:
  • 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改;
  • 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别的一致性读取,从用户的角度来看,好像是数据库可以提供同一个数据的多个版本,因此,这种技术叫做多版本并发控制(MVCC或MCC),也称为多版本数据库;
  • 4种隔离级别比较

隔离级别\读取数据一致性及允许的并发副作用

读数据一致性

脏读

不可重复读

幻读

未提交读(Read uncommitted)

最低级别,只能保证不读取物理上损坏的数据




已提交读(Read committed)

语句级




可重复读(Repeatable read)

事务级




可序列化(Serializable)

最高级别




获取InnoDB行锁争用情况

  • 可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况;
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+
5 rows in set (0.00 sec)
  • 如果发现锁争用比较严重,如InnoDB_row_lock_waitsInnoDB_row_lock_avg的值比较高,可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因;
  • 通过设置以下参数来设置监视器,MySQL定期将包含锁冲突信息的日志写入error log:
SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;
  • 也可以用以下语句来查看最新的状态信息:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
2022-05-28 18:43:39 140142237026048 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 44 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 6 srv_active, 0 srv_shutdown, 343846 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 186
OS WAIT ARRAY INFO: signal count 156
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 7481
Purge done for trx's n:o < 7455 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421617667632344, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421617667631536, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421617667630728, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
1588 OS file reads, 437 OS file writes, 234 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 4 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number          20168074
Log buffer assigned up to    20168074
Log buffer completed up to   20168074
Log written up to            20168074
Log flushed up to            20168074
Added dirty pages up to      20168074
Pages flushed up to          20168074
Last checkpoint at           20168074
91 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 524306
Buffer pool size   8192
Free buffers       7028
Database pages     1158
Old database pages 447
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 1014, created 144, written 242
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1158, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1192, Main thread ID=140142271649536 , state=sleeping
Number of rows inserted 4, updated 0, deleted 0, read 61
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
Number of system rows inserted 0, updated 317, deleted 0, read 6030
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set (0.10 sec)

ERROR: 
No query specified
  • 可以在MySQL命令行中用以下语句停止监视器:
SET GLOBAL innodb_status_output=OFF;
SET GLOBAL innodb_status_output_locks=OFF;
  • 设置监视器后,在SHOW INNODB STATUS的显示内容中,会有当前锁等待的详细信息,包括表名、锁类型、锁定记录的情况等,以便进一步分析和确定问题;
  • 打开监视器后,默认情况下每15s会向日志中记录监控内容,如果长时间打开会导致error log文件变得非常巨大,所以用户在确定问题原因之后,要及时关闭监视器。用户也可以在启动选项加入--innodb-status-file 选项使得监控信息写入指定的innodb_status.pid文件,以便和error日志进行隔离。

InnoDB行锁模式及加锁方法

  • InnoDB实现了以下两种类型的行锁:
  • 共享锁(S):允许一个事务读一行,阻止其他事务获得相同数据集的排他锁;
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁
  • 另外,为了允许行锁和表锁共存,实现多粒度机制,InnoDB还有两种内部使用的意向锁(Intention Lock),这两种意向锁都是表锁:
  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁;
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务再给一个数据行加排他锁前必须先取得该表的IX锁。
  • 锁模式的兼容情况,如下表:

请求锁模式\当前锁模式

X

IX

S

IS

X

冲突

冲突

冲突

冲突

IX

冲突

兼容

冲突

兼容

S

冲突

冲突

兼容

兼容

IS

冲突

兼容

兼容

兼 容

  • 如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放;
  • 意向锁是InnoDB自动加的,不用用户干预,对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显式给记录集加共享锁或排他锁:
    (1)在MySQL5.7中:
    共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。
    排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE。
    (2)在MySQL8.0中:
    共享锁(S):SELECT * FROM table_name WHERE … FOR SHARE。
    排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE [NOWAIT|SKIP LOCKED]