表锁(偏读) #

表锁特点:

  • 表锁偏向MyISAM存储引擎,开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。

环境准备 #

# 1、创建表
CREATE TABLE `mylock`(
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(20)
)ENGINE=MYISAM DEFAULT CHARSET=utf8 COMMENT='测试表锁';

# 2、插入数据
INSERT INTO `mylock`(`name`) VALUES('ZhangSan');
INSERT INTO `mylock`(`name`) VALUES('LiSi');
INSERT INTO `mylock`(`name`) VALUES('WangWu');
INSERT INTO `mylock`(`name`) VALUES('ZhaoLiu');

锁表的命令 #

1、查看数据库表锁的命令。

# 查看数据库表锁的命令
SHOW OPEN TABLES;

2、给mylock表上读锁,给book表上写锁。

# 给mylock表上读锁,给book表上写锁
LOCK TABLE `mylock` READ, `book` WRITE;

# 查看当前表的状态
mysql> SHOW OPEN TABLES;
+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| sql_analysis       | book                                                 |      1 |           0 |
| sql_analysis       | mylock                                               |      1 |           0 |
+--------------------+------------------------------------------------------+--------+-------------+

3、释放表锁。

# 释放给表添加的锁
UNLOCK TABLES;

# 查看当前表的状态
mysql> SHOW OPEN TABLES;
+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| sql_analysis       | book                                                 |      0 |           0 |
| sql_analysis       | mylock                                               |      0 |           0 |
+--------------------+------------------------------------------------------+--------+-------------+

读锁案例 #

1、打开两个会话,SESSION1mylock表添加读锁。

# 为mylock表添加读锁
LOCK TABLE `mylock` READ;

2、打开两个会话,SESSION1是否可以读自己锁的表?是否可以修改自己锁的表?是否可以读其他的表?那么SESSION2呢?

# SESSION1

# 问题1:SESSION1为mylock表加了读锁,可以读mylock表!
mysql> SELECT * FROM `mylock`;
+----+----------+
| id | name     |
+----+----------+
|  1 | ZhangSan |
|  2 | LiSi     |
|  3 | WangWu   |
|  4 | ZhaoLiu  |
+----+----------+
4 rows in set (0.00 sec)

# 问题2:SESSION1为mylock表加了读锁,不可以修改mylock表!
mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated

# 问题3:SESSION1为mylock表加了读锁,不可以读其他的表!
mysql> SELECT * FROM `book`;
ERROR 1100 (HY000): Table 'book' was not locked with LOCK TABLES


# SESSION2

# 问题1:SESSION1为mylock表加了读锁,SESSION2可以读mylock表!
mysql> SELECT * FROM `mylock`;
+----+----------+
| id | name     |
+----+----------+
|  1 | ZhangSan |
|  2 | LiSi     |
|  3 | WangWu   |
|  4 | ZhaoLiu  |
+----+----------+
4 rows in set (0.00 sec)

# 问题2:SESSION1为mylock表加了读锁,SESSION2修改mylock表会被阻塞,需要等待SESSION1释放mylock表!
mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

# 问题3:SESSION1为mylock表加了读锁,SESSION2可以读其他表!
mysql> SELECT * FROM `book`;
+--------+------+
| bookid | card |
+--------+------+
|      1 |    1 |
|      7 |    4 |
|      8 |    4 |
|      9 |    5 |
|      5 |    6 |
|     17 |    6 |
|     15 |    8 |
+--------+------+
24 rows in set (0.00 sec)

写锁案例 #

1、打开两个会话,SESSION1mylock表添加写锁。

# 为mylock表添加写锁
LOCK TABLE `mylock` WRITE;

2、打开两个会话,SESSION1是否可以读自己锁的表?是否可以修改自己锁的表?是否可以读其他的表?那么SESSION2呢?

# SESSION1

# 问题1:SESSION1为mylock表加了写锁,可以读mylock的表!
mysql> SELECT * FROM `mylock`;
+----+----------+
| id | name     |
+----+----------+
|  1 | ZhangSan |
|  2 | LiSi     |
|  3 | WangWu   |
|  4 | ZhaoLiu  |
+----+----------+
4 rows in set (0.00 sec)

# 问题2:SESSION1为mylock表加了写锁,可以修改mylock表!
mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# 问题3:SESSION1为mylock表加了写锁,不能读其他表!
mysql> SELECT * FROM `book`;
ERROR 1100 (HY000): Table 'book' was not locked with LOCK TABLES

# SESSION2

# 问题1:SESSION1为mylock表加了写锁,SESSION2读mylock表会阻塞,等待SESSION1释放!
mysql> SELECT * FROM `mylock`;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

# 问题2:SESSION1为mylock表加了写锁,SESSION2读mylock表会阻塞,等待SESSION1释放!
mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

# 问题3:SESSION1为mylock表加了写锁,SESSION2可以读其他表!
mysql> SELECT * FROM `book`;
+--------+------+
| bookid | card |
+--------+------+
|      1 |    1 |
|      7 |    4 |
|      8 |    4 |
|      9 |    5 |
|      5 |    6 |
|     17 |    6 |
|     15 |    8 |
+--------+------+
24 rows in set (0.00 sec)

案例结论 #

MyISAM引擎在执行查询语句SELECT之前,会自动给涉及到的所有表加读锁,在执行增删改之前,会自动给涉及的表加写锁。

MySQL的表级锁有两种模式:

  • 表共享读锁(Table Read Lock)。

  • 表独占写锁(Table Write Lock)。

MyISAM表进行操作,会有以下情况:

  • MyISAM表的读操作(加读锁),不会阻塞其他线程対同一表的读操作,但是会阻塞其他线程対同一表的写操作。只有当读锁释放之后,才会执行其他线程的写操作。
  • MyISAM表的写操作(加写锁),会阻塞其他线程対同一表的读和写操作,只有当写锁释放之后,才会执行其他线程的读写操作。

表锁分析 #

mysql> SHOW STATUS LIKE 'table%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 173   |
| Table_locks_waited         | 0     |
| Table_open_cache_hits      | 5     |
| Table_open_cache_misses    | 8     |
| Table_open_cache_overflows | 0     |
+----------------------------+-------+
5 rows in set (0.00 sec)

可以通过Table_locks_immediateTable_locks_waited状态变量来分析系统上的表锁定。具体说明如下:

Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1。

Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在较严重的表级锁争用情况。

此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合作为主表的引擎。因为写锁后,其他线程不能进行任何操作,大量的写操作会使查询很难得到锁,从而造成永远阻塞。

行锁(偏写) #

行锁特点:

  • 偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。

InnoDB存储引擎和MyISAM存储引擎最大不同有两点:一是支持事务,二是采用行锁。

事务的ACID:

  • Atomicity [ˌætəˈmɪsəti]
  • Consistency [kənˈsɪstənsi]
  • Isolation [ˌaɪsəˈleɪʃn]
  • Durability [ˌdjʊərəˈbɪlɪti]

环境准备 #

# 建表语句
CREATE TABLE `test_innodb_lock`(
`a` INT,
`b` VARCHAR(16)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='测试行锁'; 

# 插入数据
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(1, 'b2');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(2, '3');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(3, '4000');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(4, '5000');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(5, '6000');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(6, '7000');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(7, '8000');
INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(8, '9000');

# 创建索引
CREATE INDEX idx_test_a ON `test_innodb_lock`(a);
CREATE INDEX idx_test_b ON `test_innodb_lock`(b);

行锁案例 #

1、开启手动提交

打开SESSION1SESSION2两个会话,都开启手动提交。

# 开启MySQL数据库的手动提交
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)

2、读几知所写

# SESSION1 

# SESSION1対test_innodb_lock表做写操作,但是没有commit。
# 执行修改SQL之后,查询一下test_innodb_lock表,发现数据被修改了。
mysql> UPDATE `test_innodb_lock` SET `b` = '88' WHERE `a` = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM `test_innodb_lock`;
+------+------+
| a    | b    |
+------+------+
|    1 | 88   |
|    2 | 3    |
|    3 | 4000 |
|    4 | 5000 |
|    5 | 6000 |
|    6 | 7000 |
|    7 | 8000 |
|    8 | 9000 |
+------+------+
8 rows in set (0.00 sec)

# SESSION2 

# SESSION2这时候来查询test_innodb_lock表。
# 发现SESSION2是读不到SESSION1未提交的数据的。
mysql> SELECT * FROM `test_innodb_lock`;
+------+------+
| a    | b    |
+------+------+
|    1 | b2   |
|    2 | 3    |
|    3 | 4000 |
|    4 | 5000 |
|    5 | 6000 |
|    6 | 7000 |
|    7 | 8000 |
|    8 | 9000 |
+------+------+
8 rows in set (0.00 se

3、行锁两个SESSION同时対一条记录进行写操作

# SESSION1 対test_innodb_lock表的`a`=1这一行进行写操作,但是没有commit
mysql> UPDATE `test_innodb_lock` SET `b` = '99' WHERE `a` = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# SESSION2 也对test_innodb_lock表的`a`=1这一行进行写操作,但是发现阻塞了!!!
# 等SESSION1执行commit语句之后,SESSION2的SQL就会执行了
mysql> UPDATE `test_innodb_lock` SET `b` = 'asdasd' WHERE `a` = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

4、行锁两个SESSION同时对不同记录进行写操作

# SESSION1 対test_innodb_lock表的`a`=6这一行进行写操作,但是没有commit
mysql> UPDATE `test_innodb_lock` SET `b` = '8976' WHERE `a` = 6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# SESSION2 対test_innodb_lock表的`a`=4这一行进行写操作,没有阻塞!!!
# SESSION1和SESSION2同时对不同的行进行写操作互不影响
mysql> UPDATE `test_innodb_lock` SET `b` = 'Ringo' WHERE `a` = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

索引失效行锁变表锁 #

# SESSION1 执行SQL语句,没有执行commit。
# 由于`b`字段是字符串,但是没有加单引号导致索引失效
mysql> UPDATE `test_innodb_lock` SET `a` = 888 WHERE `b` = 8000;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1

# SESSION2 和SESSION1操作的并不是同一行,但是也被阻塞了???
# 由于SESSION1执行的SQL索引失效,导致行锁升级为表锁。
mysql> UPDATE `test_innodb_lock` SET `b` = '1314' WHERE `a` = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

where条件里面没有使用索引,那么就变成了全表扫描,update 语句也变成了表锁.

间隙锁的危害 #

什么是间隙锁?

当我们用范围条件而不是相等条件检索数据,并请求共享或者排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范文内但并不存在的记录,叫做"间隙(GAP)"。

InnoDB也会对这个"间隙"加锁,这种锁的机制就是所谓的"间隙锁"。

间隙锁的危害

因为Query执行过程中通过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值不存在。

间隙锁有一个比较致命的缺点,就是**当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。**在某些场景下这可能会対性能造成很大的危害。

如何锁定一行 #

锁定一行

SELECT .....FOR UPDATE在锁定某一行后,其他写操作会被阻塞,直到锁定的行被COMMIT

案例结论 #

InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。

但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

行锁分析 #

mysql> SHOW STATUS LIKE 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 124150 |
| Innodb_row_lock_time_avg      | 31037  |
| Innodb_row_lock_time_max      | 51004  |
| Innodb_row_lock_waits         | 4      |
+-------------------------------+--------+
5 rows in set (0.00 sec)

対各个状态量的说明如下:

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量。
  • Innodb_row_lock_time:从系统启动到现在锁定总时间长度(重要)。
  • Innodb_row_lock_time_avg:每次等待所花的平均时间(重要)。
  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间。
  • Innodb_row_lock_waits:系统启动后到现在总共等待的次数(重要)。

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化策略。

死锁 #

InnoDB 实现了以下两种类型的行锁:

    1. 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
    1. 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

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

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

20201204193446

(如果一个事务请求的锁模式与当前的锁兼容, InnoDB 就将请求的锁授予该事务; 反之, 如果两者不兼容,该事务就要等待锁释放。)

  • InnoDB加锁方法:
    • 对于普通SELECT语句,InnoDB 不会加任何锁
    • 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB会自动给涉及数据集加排他锁(X)
    • 事务可以通过以下语句显式给记录集加共享锁或排他锁:
      • 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 其他 session 仍然可以查询记录,并也可以对该记录加 share mode 的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。
      • 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。其他 session 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁
    • 意向锁是 InnoDB 自动加的, 不需用户干预。

死锁有三种情况

  • 第一种情况,用户1访问表A(锁住A表),然后访问表B,用户2,访问表B(锁住B),然后访问表A,这样用户1等待用户2释放表B上的锁,用户2等待用户1释放表A上的锁!

解决方法: 主要由于逻辑程序bug产生,在事务的执行顺序上,如果有多个资源,保证代码执行资源顺序的一致性,比如事务A和事务B都是更新A表后然后执行B表,那么都按这个顺序执行,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。

  • 第二种情况,事务a查询一条记录,然后准备更新这条记录,事务b进行同样的操作,但由于速度过快,事务a没来来得及commit,那么事务a查询获得这条记录的共享锁,并且事务a准备更新获得排他锁,此时b同样获得该条记录的共享锁,事务a等待事务b释放锁,进行更新,同样事务b等待事务a执行完进行更新,产生死锁!场景如下,如果一个按钮没有失效设置,用户一直点,就会出现上述情况 20201204185457

解决方法:

  1. 按钮点击置灰
  2. 使用悲观锁,select for update  在查询的时候就加排他锁,那么事务b就不能进行查询获得共享锁
  3. 使用乐观锁,在每条数据加版本号version字段,对所读操作的数据都不加锁,事务a和事务b第一部查出相应的版本号v,在更新前在把提交数据与数据库版本比较,例如,update set where (v+1)>version,如果不大于认为过期就不进行更新了
  • 第三种情况,如果使用一条update不满足条件的sql语句,那么行锁变成了表锁,

比如索引失效情况,用了is null或者type建立索引或者使用了like %,或者的话where条件里面没有使用索引,那么就变成了全表扫描,update 语句也变成了表锁,那么这样的事务过多,造成死锁或阻塞!

InnoDB加锁方法

数据一致性 #

假设有A、B两个用户同时各购买一件 id=1 的商品,用户A获取到的库存量为 1000,用户B获取到的库存量也为 1000,用户A完成购买后修改该商品的库存量为 999,用户B完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。

有两种解决方案:

悲观锁方案:每次获取商品时,对该商品加排他锁。也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录。悲观锁适合写入频繁的场景。

begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;

乐观锁方案:每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

# 不加锁获取 id=1 的商品对象
select * from goods where id = 1

begin;
# 更新 stock 值,这里需要注意 where 条件 stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;

如果我们需要设计一个商城系统,该选择以上的哪种方案呢?

查询商品的频率比下单支付的频次高,基于以上我可能会优先考虑第二种方案(当然还有其他的方案,这里只考虑以上两种方案)。

但是第二种的话,要注意下它的先开始查询,然后开始事物,可能事物里面有许多的其他查询耗时。