016、锁实验

 

MySQL锁实验

MySQL的锁是基于索引的
锁定算法,MySQL的innodb存储引擎有3种锁的算法:
  1. Record lock:单个行记录上的锁
  2. Gap lock:间隙锁,锁定一个范围,但不包括记录本身
  3. Next-key lock:Gap lock + record lock锁定一个范围,并且锁定记录本身。Next-key lock是为了解决幻读,这是innodb默认的锁算法。
在innodb中,基于主键的行锁只有record lock的,没有gap lock,因为主键可以锁定唯一记录。
在innodb中,基于非唯一索引的辅助索引的锁,除了record lock,还有gap lock,也就是next-key lock。
行锁升级为表锁的情况在innodb中,如果会话是基于没有索引列的条件修改数据(where条件中的列没有加索引)时,在RR隔离级别下,有可能会导致所有的行记录被锁住,升级为表锁。
阻塞:因为不同锁之间的兼容性关系,在某些时刻,一个事务中的锁需要等待另一个事务中的锁释放它占用的资源。在innodb存储引擎源码中,用mutex数据结构来实现锁。在访问资源前需要用mutex_enter函数进行申请,在访问或修改完毕后立即执行mutex_exit函数。当一个资源被一个事务占有时,另一个事务执行mutex_enter函数就会发生等待,这既是阻塞。
事务阻塞等待时间:innodb存储引擎使用innodb_lock_wait_timeout来控制等待时间,默认50s。
mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.00 sec)
 

实验一:Record lock

基于主键的DML操作只会锁单个记录,实验过程如下,创建实验表:
mysql> create table t(
    ->   a int(11) not null,
    ->   b varchar(20) default null,
    ->   c varchar(10) default null,
    ->   primary key(a),
    ->   key b(b)
    -> );
Query OK, 0 rows affected (0.33 sec)
--该表中主键没有满足主键三个特性:非空,自增,唯一
插入测试数据:
mysql> insert into t values(1,'aa','bb');
Query OK, 1 row affected (0.32 sec)

mysql> insert into t values(2,'cc','dd');
Query OK, 1 row affected (0.01 sec)

mysql> insert into t values(3,'ee','ff');
Query OK, 1 row affected (0.01 sec)

mysql> select * from t;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 1 | aa   | bb   |
| 2 | cc   | dd   |
| 3 | ee   | ff   |
+---+------+------+
3 rows in set (0.00 sec)
开启两个事务,会话1和会话2,会话1更新数据,先不提交,会话2更新同一行数据:
mysql> begin;--会话1开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> begin;--会话2开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> update t set b='aaa' where a=1; --会话1更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update t set b='aaaa' where a=1; --会话2更新数据,发现一直在等待,等到默认超时时间时,结束
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

--会话2查询数据,并没有更新
mysql> select * from t;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 1 | aa   | bb   |
| 2 | cc   | dd   |
| 3 | ee   | ff   |
+---+------+------+
3 rows in set (0.00 sec)
会话2更新不同于会话1的行,并不会有锁等待:
mysql> update t set b='ccc' where a=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
说明基于主键的DML操作锁定单个记录。
这就是第一种锁算法,Record lock:单个行记录上的锁

实验二:行锁升级为表锁

行锁升级为表锁的情况在innodb中,如果会话是基于没有索引列的条件修改数据时,在RR隔离级别下,有可能会导致所有的行记录被锁住,升级为表锁。
创建测试表,插入测试数据:
mysql> create table t(id int);
Query OK, 0 rows affected (0.21 sec)

mysql> insert into t values(1),(2),(3),(4);
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> select * from t;
+------+
| id   |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
+------+
4 rows in set (0.00 sec)
会话1开启事务,并进行更新操作,先不提交:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t set id=111 where id=1;
Query OK, 1 row affected (0.31 sec)
Rows matched: 1  Changed: 1  Warnings: 0
会话2开启事务,更新不同行操作:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t set id=222 where id=2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
会话3查看锁堵塞(在会话2等待超时之前查看):
mysql> use information_schema;
Database changed

mysql> select * from INNODB_LOCKS;
+---------------+-------------+-----------+-----------+------------+-----------------+------------+-----------+----------+----------------+
| lock_id       | lock_trx_id | lock_mode | lock_type | lock_table | lock_index      | lock_space | lock_page | lock_rec | lock_data      |
+---------------+-------------+-----------+-----------+------------+-----------------+------------+-----------+----------+----------------+
| 275745:87:3:2 | 275745      | X         | RECORD    | `test`.`t` | GEN_CLUST_INDEX |         87 |         3 |        2 | 0x000000000B00 |
| 275744:87:3:2 | 275744      | X         | RECORD    | `test`.`t` | GEN_CLUST_INDEX |         87 |         3 |        2 | 0x000000000B00 |
+---------------+-------------+-----------+-----------+------------+-----------------+------------+-----------+----------+----------------+
2 rows in set (0.00 sec)

mysql> select * from INNODB_LOCK_WAITS;
+-------------------+-------------------+-----------------+------------------+
| requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id |
+-------------------+-------------------+-----------------+------------------+
| 275745            | 275745:87:3:2     | 275744          | 275744:87:3:2    |
+-------------------+-------------------+-----------------+------------------+
1 row in set (0.00 sec)
以上操作发现,更新不同行的数据也会有锁等待现象。
结论:mysql innodb的锁是加在索引身上的,如果该表没有主键,索引的情况。innodb会由行锁升级为表锁(会锁定所有行的记录)。

实验三:Gap lock和next-key lock

创建实验表,并插入数据:
mysql> create table t(
    ->   a int,
    ->   primary key(a)
    -> );
Query OK, 0 rows affected (0.04 sec)

mysql> insert into t values(1),(2),(3),(4),(7),(8);
Query OK, 6 rows affected (0.06 sec)
Records: 6  Duplicates: 0  Warnings: 0

mysql> select * from t;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 7 |
| 8 |
+---+
6 rows in set (0.00 sec)
会话1开启事务,并以共享锁的形式查询:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t where a<6 lock in share mode;
+---+
| a |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
+---+
4 rows in set (0.00 sec)
会话2开启事务,向表中插入数据,该数据满足会话1的查询条件:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t select 5;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> insert into t select 6;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
会话3查询锁等待(在会话2等待超时之前查看):
mysql> select * from INNODB_LOCK_WAITS;
+-------------------+-------------------+-----------------+------------------+
| requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id |
+-------------------+-------------------+-----------------+------------------+
| 275773            | 275773:88:3:6     | 275771          | 275771:88:3:6    |
+-------------------+-------------------+-----------------+------------------+
1 row in set (0.00 sec)

mysql> select * from INNODB_LOCKS;
+---------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+
| lock_id       | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
+---------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+
| 275773:88:3:6 | 275773      | X,GAP     | RECORD    | `test`.`t` | PRIMARY    |         88 |         3 |        6 | 7         |
| 275771:88:3:6 | 275771      | S         | RECORD    | `test`.`t` | PRIMARY    |         88 |         3 |        6 | 7         |
+---------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+
2 rows in set (0.00 sec)
这种情况下,不论插入的记录是5还是6,都会被锁定,因为在next-key lock算法下,锁定的是小于6这个数值区间的所有数值。但是插入数值9是可以的,因为该记录不在锁定的范围内。
mysql> insert into t select 9;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0
而对于单个值的索引查询,不需要用到Gap lock,只需要加一个record lock即可,因此innodb存储引擎会自己选一个最小的算法模型。

以上步骤验证了间隙锁的特性:
GAP间隙锁:锁定范围,不包含记录的本身
next-key lock:既锁定范围,又锁定记录的本身。 lock in share mode   避免出现幻读现象
 

实验四:死锁

如果程序是串行的,那么不可能发生死锁,死锁只发生与并发情况,数据库就是一个并发进行着的程序,因此可能发生死锁。
innodb存储引擎有一个后台的锁监控线程,该线程负责查看可能的死锁问题,并自动告知用户。
 
死锁产生的原因:会话A和会话B的资源互相在等待。
大多数的死锁,innodb存储引擎自己可以侦测到,不需要人为进行干预。
发生死锁后,会话B事务抛出死锁异常,并且自动回滚,这时会话A马上得到之前会话B锁定的资源。
演示死锁的产生,实验过程如下,创建实验表:
mysql> create table t(
    ->   id int not null auto_increment,
    ->   username varchar(20) default null,
    ->   primary key(id),
    ->   key username(username)
    -> );
Query OK, 0 rows affected (0.06 sec)
插入测试数据:
mysql> insert into t values(2,'aa');
Query OK, 1 row affected (0.01 sec)

mysql> insert into t values(4,'aa');
Query OK, 1 row affected (0.03 sec)

mysql> select * from t;
+----+----------+
| id | username |
+----+----------+
|  2 | aa       |
|  4 | aa       |
+----+----------+
2 rows in set (0.00 sec)
会话1开启事务,执行更新操作,更新id=2的数据,先不提交:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t set username='aaa' where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
会话2开启事务,执行更新操作,id=4的数据,先不提交:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t set username='aaa' where id=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
到目前为止,两个会话锁定的资源为不同的行,没有冲突。
会话1更新数据,更新id=4的数据(id=4的数据当前被会话2锁定):
mysql> update t set username='bb' where id=4;
--一直在等待
会话2更新数据,更新id=2的数据(id=2的数据当前被会话1锁定):
mysql> update t set username='ccc' where id=2;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
发现死锁,会话2自动回滚,会话1执行更新操作:
mysql> update t set username='bb' where id=4;
Query OK, 1 row affected (10.93 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t;
+----+----------+
| id | username |
+----+----------+
|  2 | aaa      |
|  4 | bb       |
+----+----------+
2 rows in set (0.00 sec)
结论:出现死锁,数据库自动回滚。不会等待。
死锁出现的原因:两个会话互相请求(交叉请求)对方已锁定的资源,进行DML操作时发生。

监控死锁

查看innodb存储引擎状态可以查看到死锁的信息,在操作系统层面结合grep过滤:
[root@localhost ~]# mysql -uroot -proot -e "show engine innodb status \G;"|grep DEAD
Warning: Using a password on the command line interface can be insecure.
LATEST DETECTED DEADLOCK
死锁建议放在监控中,实时查看。
 

如何提高insert插入的效率

假如现在有一个sql脚本,里面都是insert语句,数据量有1000万行,如何快速插入到数据库中。
1、在数据库中调用脚本,而不是把脚本内容复制粘贴执行:
mysql> source /u01/scripts/1.sql
2、由于MySQL数据库是自动提交的,可以在脚本开头加begin;在脚本结尾加commit,将这1000万数据放在一个事务中,这样就避免了事务的频繁提交。
 





posted @ 2021-06-22 17:14  有点菜大人  阅读(94)  评论(0)    收藏  举报