InnoDB的行锁模式及加锁方法

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

l  共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

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

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

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

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

上述锁模式的兼容情况具体如表20-6所示。

表20-6                                            InnoDB行锁模式兼容性列表

请求锁模式

   是否兼容

当前锁模式

X

IX

S

IS

X

冲突

冲突

冲突

冲突

IX

冲突

兼容

冲突

兼容

S

冲突

冲突

兼容

兼容

IS

冲突

兼容

兼容

兼容

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

意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。

¡  共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。

¡  排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。

用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。

在如表20-7所示的例子中,使用了SELECT ... IN SHARE MODE加锁后再更新记录,看看会出现什么情况,其中actor表的actor_id字段为主键。

表20-7                     InnoDB存储引擎的共享锁例子

session_1

session_2

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select actor_id,first_name,last_name from actor where actor_id = 178;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select actor_id,first_name,last_name from actor where actor_id = 178;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

当前session对actor_id=178的记录加share mode 的共享锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.01 sec)

 
 

其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.01 sec)

当前session对锁定的记录进行更新操作,等待锁:

mysql> update actor set last_name = 'MONROE T' where actor_id = 178;

等待

 
 

其他session也对该记录进行更新操作,则会导致死锁退出:

mysql> update actor set last_name = 'MONROE T' where actor_id = 178;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

获得锁后,可以成功更新:

mysql> update actor set last_name = 'MONROE T' where actor_id = 178;

Query OK, 1 row affected (17.67 sec)

Rows matched: 1  Changed: 1  Warnings: 0

 

    当使用SELECT...FOR UPDATE加锁后再更新记录,出现如表20-8所示的情况。

表20-8                               InnoDB存储引擎的排他锁例子

session_1

session_2

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select actor_id,first_name,last_name from actor where actor_id = 178;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select actor_id,first_name,last_name from actor where actor_id = 178;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

当前session对actor_id=178的记录加for update的共享锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

 
 

其他session可以查询该记录,但是不能对该记录加共享锁,会等待获得锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 178;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE    |

+----------+------------+-----------+

1 row in set (0.00 sec)

mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;

等待

当前session可以对锁定的记录进行更新操作,更新后释放锁:

mysql> update actor set last_name = 'MONROE T' where actor_id = 178;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;

Query OK, 0 rows affected (0.01 sec)

 
 

其他session获得锁,得到其他session提交的记录:

mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

| 178      | LISA       | MONROE T  |

+----------+------------+-----------+

1 row in set (9.59 sec)

InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。

(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。

在如表20-9所示的例子中,开始tab_no_index表没有索引:

mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;

Query OK, 0 rows affected (0.15 sec)

mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');

Query OK, 4 rows affected (0.00 sec)

Records: 4  Duplicates: 0  Warnings: 0

表20-9               InnoDB存储引擎的表在不使用索引时使用表锁例子             

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_no_index where id = 1 ;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

+------+------+

1 row in set (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_no_index where id = 2 ;

+------+------+

| id   | name |

+------+------+

| 2    | 2    |

+------+------+

1 row in set (0.00 sec)

mysql> select * from tab_no_index where id = 1 for update;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

+------+------+

1 row in set (0.00 sec)

 
 

mysql> select * from tab_no_index where id = 2 for update;

等待

在如表20-9所示的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待!原因就是在没有索引的情况下,InnoDB只能使用表锁。当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如表20-10所示。

创建tab_with_index表,id字段有普通索引:

mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;

Query OK, 0 rows affected (0.15 sec)

mysql> alter table tab_with_index add index id(id);

Query OK, 4 rows affected (0.24 sec)

Records: 4  Duplicates: 0  Warnings: 0

表20-10                                InnoDB存储引擎的表在使用索引时使用行锁例子

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_with_index where id = 1 ;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

+------+------+

1 row in set (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_with_index where id = 2 ;

+------+------+

| id   | name |

+------+------+

| 2    | 2    |

+------+------+

1 row in set (0.00 sec)

mysql> select * from tab_with_index where id = 1 for update;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

+------+------+

1 row in set (0.00 sec)

 
 

mysql> select * from tab_with_index where id = 2 for update;

+------+------+

| id   | name |

+------+------+

| 2    | 2    |

+------+------+

1 row in set (0.00 sec)

(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。

在如表20-11所示的例子中,表tab_with_index的id字段有索引,name字段没有索引:

mysql> alter table tab_with_index drop index name;

Query OK, 4 rows affected (0.22 sec)

Records: 4  Duplicates: 0  Warnings: 0

mysql> insert into tab_with_index  values(1,'4');

Query OK, 1 row affected (0.00 sec)

mysql> select * from tab_with_index where id = 1;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

| 1    | 4    |

+------+------+

2 rows in set (0.00 sec)

表20-11                InnoDB存储引擎使用相同索引键的阻塞例子       

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_with_index where id = 1 and name = '1' for update;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

+------+------+

1 row in set (0.00 sec)

 
 

虽然session_2访问的是和session_1不同的记录,但是因为使用了相同的索引,所以需要等待锁:

mysql> select * from tab_with_index where id = 1 and name = '4' for update;

等待

(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

在如表20-12所示的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:

mysql> alter table tab_with_index add index name(name);

Query OK, 5 rows affected (0.23 sec)

Records: 5  Duplicates: 0  Warnings: 0

表20-12                                  InnoDB存储引擎的表使用不同索引的阻塞例子

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from tab_with_index where id = 1 for update;

+------+------+

| id   | name |

+------+------+

| 1    | 1    |

| 1    | 4    |

+------+------+

2 rows in set (0.00 sec)

 
 

Session_2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁:

mysql> select * from tab_with_index where name = '2' for update;

+------+------+

| id   | name |

+------+------+

| 2    | 2    |

+------+------+

1 row in set (0.00 sec)

 

由于访问的记录已经被session_1锁定,所以等待获得锁。:

mysql> select * from tab_with_index where name = '4' for update;

(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。关于MySQL在什么情况下不使用索引的详细讨论,参见本章“索引问题”一节的介绍。

在下面的例子中,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。

例子中tab_with_index表的name字段有索引,但是name字段是varchar类型的,如果where条件中不是和varchar类型进行比较,则会对name进行类型转换,而执行的全表扫描。

mysql> alter table tab_no_index add index name(name);

Query OK, 4 rows affected (8.06 sec)

Records: 4  Duplicates: 0  Warnings: 0

mysql> explain select * from tab_with_index where name = 1 /G

*************************** 1. row ***************************

           id: 1

  select_type: SIMPLE

        table: tab_with_index

         type: ALL

possible_keys: name

          key: NULL

      key_len: NULL

          ref: NULL

         rows: 4

        Extra: Using where

1 row in set (0.00 sec)

mysql> explain select * from tab_with_index where name = '1' /G

*************************** 1. row ***************************

           id: 1

  select_type: SIMPLE

        table: tab_with_index

         type: ref

possible_keys: name

          key: name

      key_len: 23

          ref: const

         rows: 1

        Extra: Using where

1 row in set (0.00 sec)

InnoDB 存储引擎中,**(Row-Level Locking)是并发控制的重要机制**,用于在多用户并发访问数据库时,确保事务的隔离性和一致性。InnoDB 支持,这是相对于表级更细粒度的机制,能大大提高并发性能。 --- ## ✅ InnoDB 的实现原理 InnoDB并不是通过单独的对象来实现的,而是**通过索引项上的机制实现的**。也就是说,**InnoDB实际上是“索引记录”**。 ### 🔍 InnoDB 的实现依赖于以下核心机制: 1. **索引结构**: - InnoDB 使用 B+ 树索引结构。 - 每个索引记录(index record)都可以被加锁。 - 是加在索引记录上的,而不是物理上。 2. **的类型**: InnoDB 支持多种模式,常见的类型包括: - **记录(Record Lock)**:住一条索引记录。 - **间隙(Gap Lock)**:住一个范围,但不包括记录本身(用于防止幻读)。 - **临键(Next-Key Lock)**:记录 + 间隙定一个范围,并包括记录本身。 3. **的存储结构**: - 每个事务在请求时,InnoDB 会在内存中维护一个结构(`lock0sys.h` 中定义)。 - 每个结构包含事务信息、的类型、定的索引记录等信息。 4. **的兼容性**: - InnoDB 使用兼容矩阵来判断多个事务的请求是否冲突。 - 例如,两个事务不能同时对同一记录加排他(X),但可以同时加共享(S)。 --- ## ✅ 的实现细节 ### 1. 记录(Record Lock) ```sql -- 假设有主键索引 id SELECT * FROM users WHERE id = 1 FOR UPDATE; ``` - InnoDB 会在主键索引中定 `id = 1` 这条记录。 - 其他事务不能对这条记录进修改或加排他。 ### 2. 间隙(Gap Lock) ```sql -- 假设 id 是主键,当前表中没有 id = 5 的记录 SELECT * FROM users WHERE id BETWEEN 3 AND 7 FOR UPDATE; ``` - InnoDB定 `(3,7)` 这个范围,防止其他事务插入 `id=5`。 - 这是防止“幻读”的关键机制。 ### 3. 临键(Next-Key Lock) - 是记录和间隙的组合。 - 定一个索引记录及其前面的间隙。 - InnoDB 默认使用 Next-Key Lock 来避免幻读问题。 --- ## ✅ 示例:演示如何在事务中工作 ```python # Python 示例:使用 pymysql 模拟两个事务对同一加锁 import pymysql import threading def transaction1(): conn = pymysql.connect(host='localhost', user='root', password='123456', db='testdb') cur = conn.cursor() try: conn.begin() cur.execute("UPDATE users SET name = 'Alice' WHERE id = 1") print("Transaction 1: Updated id=1") input("Transaction 1: Press Enter to commit...") conn.commit() finally: cur.close() conn.close() def transaction2(): conn = pymysql.connect(host='localhost', user='root', password='123456', db='testdb') cur = conn.cursor() try: conn.begin() print("Transaction 2: Trying to update id=1...") cur.execute("UPDATE users SET name = 'Bob' WHERE id = 1") print("Transaction 2: Updated id=1") conn.commit() except Exception as e: print("Transaction 2: Error:", e) finally: cur.close() conn.close() # 启动两个事务 t1 = threading.Thread(target=transaction1) t2 = threading.Thread(target=transaction2) t1.start() t2.start() t1.join() t2.join() ``` 在这个例子中: - 事务1先对 `id=1` 加排他; - 事务2尝试更新同一时会被阻塞,直到事务1提交或回滚; - 如果事务1回滚,事务2可以继续执; - 如果事务1提交,事务2继续执。 --- ## ✅ InnoDB 的特点 | 特性 | 描述 | |------|------| | 粒度 | ,粒度最小,适合高并发写操作 | | 死 | 可能发生,InnoDB 有死检测机制 | | 开销 | 较大,需要维护结构 | | 适用场景 | 高并发写入、更新、删除操作 | --- ## ✅ 小结 - InnoDB是基于索引记录实现的。 - 的类型包括记录、间隙、临键。 - 提高了并发性能,但也带来了死等待的问题。 - 要避免带来的性能问题,应合理设计索引,减少冲突。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值