MySQL(二十)锁(三)乐观锁与悲观锁、显示隐式锁和其他锁

MySQL(二十)锁(三)乐观锁与悲观锁 显式锁和隐式锁

1 从对待锁的态度划分:乐观锁、悲观锁

  • 从对待锁的态度划分,可以将锁划分为乐观锁和悲观锁,可以看出这两种锁是两种对待数据并发的思维方式
  • 乐观锁和悲观锁并不是锁,而是锁的设计思想
1.1 乐观锁
  • 乐观锁对数据被其他事务修改持有乐观态度
  • 每次不会对数据上锁,而是在更新的时候判断一下其他事务有没有更新这个数据
  • 不需要借助数据库本身的锁机制,而是通过程序实现,如版本号机制或者cas机制
  • 乐观锁适用于多读的场景,能够提高吞吐量
乐观锁的版本号机制

​ 在表中设计一个version字段,每次在更新的时候更新一下这个字段,如果发生变化则更新失败,重新获取字段进行尝试:

update ... set version = version + 1 where `version` = version;
1.2 悲观锁
  • 悲观锁是一种思想,即对数据被其他事务修改持有悲观态度
  • 会通过数据库本身的锁机制(比如读写锁、表锁、行锁等)在每次获取数据的时候都进行加锁,其他事务想要获取数据就会堵塞直到它拿到锁。
  • 特别对于长事务,悲观锁对于数据库性能开销影响很大

注意点:

  • SQL语句SELECT ... FOR UPDATE就是MySQL中的悲观锁,会将扫描过程中的所有行都加锁

    这是因为MySQL加行锁是加到索引上的,所以如果使用锁没有用到索引,则会进行全表扫描,将全表索引都加上锁

  • 当一个事务执行SELECT ... FOR UPDATE,另一个事务执行SELECT ... 并不会收到影响,这是因为InnoDB存储引擎采取的是快照读,不会收到锁的影响

2 按照加锁的方式划分:隐式锁和显式锁

🌟2.1 隐式锁

​ 一个事务如果在执行insert操作的时候,如果插入的间隙被添加了gap锁,则这个事务会堵塞,并且当前事务会在该间隙上插入一个插入意向锁,而如果没有gap锁,则一般情况下insert操不会添加任何的锁。如果一个事务首先插入了一条记录(此时没有在内存生产与该记录关联的表结构),然后另一个事务

  • 立即查看这条记录,即获取S锁或者X锁,则可能导致脏读问题
  • 立即修改这条记录,即获取X锁,就可能导致脏写问题

​ 之前讲过,Compact行格式有三个隐藏列:row_idtrx_id回滚指针,其中的事务idtrx_id就用于解决上面的问题:

  • 情景一:对于聚簇索引来说,会有一个trx_id记录着最后改动该记录的事务id,如果当前事务插入了一条聚簇索引记录后,该记录的trx_id就会变成该事务的id,如果此时其他事务想要对该记录添加X锁或者S锁,就会先查看一下trx_id对应的事务是否处于活跃状态(即is_waiting是否为false),如果是,则帮助活跃的事务创建一个X锁(即给活跃的事务创建一个锁结构,锁结构的is_waiting为false),然后自己进入等待状态(即给当前事务创建一个锁结构,锁结构的is_waiting为true)
  • 情景二:对于二级索引来说,本身没有trx_id属性,但是在二级索引页面的Page Header中记录的Page_max_trx_id表示对该页面做改动的最大的事务id,如果当前正在活跃的事务id大于该Page_max_trx_id,则表明“对页面进行修改的事务都已经提交了”,否则需要在页面中定位到对应的二级索引,通过回表找到聚簇索引重新执行情景一

​ 总结来说,一个事务对新插入的记录可以不显式地加锁,因为有trx_id的存在,相当于加了一个隐式锁,其他事务想要对该记录添加X锁 或者S锁的时候,需要查看一下trx_id对应的事务是否活跃,如果活跃则需要帮助活跃事务创建一个X锁的内存结构,并也给自己添加X锁陷入等待。

​ 隐式锁采用如上的延迟加锁的机制,目的是为了减少锁的数量。

测试

​ 事务一执行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into student1 values(7, '7', '7');
Query OK, 1 row affected (0.00 sec)

mysql> 

​ 事务二执行,发现出现了堵塞

Database changed
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 正常查看是因为InnoDB的快照读
mysql> select * from student1 where stu_no = 7;
Empty set (0.00 sec)

-- 因为隐式锁的存在,加锁失败
mysql> select * from student1 where stu_no = 7 lock in share mode;

select * from performance_schema.data_lock_waits查看目前堵塞的事务,能够查看到也可以说隐式锁转化为了显示锁

mysql> select * from performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 140068966219776:183:4:8:140068882800792
REQUESTING_ENGINE_TRANSACTION_ID: 57127
            REQUESTING_THREAD_ID: 3637
             REQUESTING_EVENT_ID: 26
REQUESTING_OBJECT_INSTANCE_BEGIN: 140068882800792
         BLOCKING_ENGINE_LOCK_ID: 140068966218968:183:4:8:140068882794272
  BLOCKING_ENGINE_TRANSACTION_ID: 57122
              BLOCKING_THREAD_ID: 3637
               BLOCKING_EVENT_ID: 25
  BLOCKING_OBJECT_INSTANCE_BEGIN: 140068882794272
1 row in set (0.00 sec)

ERROR: 
No query specified

2.2 显式锁

​ 通过特定语句加的锁

... for share | lock in share mode;
... for update;

3 其他锁

3.1 全局锁
  • 全局锁就是对整个数据库实例进行加锁,使得整个数据库处于只读状态
  • 其他线程下的语句处于堵塞状态
Flush tables with read lock;
3.2 死锁

​ 两个事务都持有对方需要的锁,并且都不释放自己持有的锁等待对方释放导致的互相等待的现象。

举例:

​ 事务一运行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance - 10 where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set balance = balance + 10 where id = 2;
Query OK, 1 row affected (7.63 sec)
Rows matched: 1  Changed: 1  Warnings: 0

​ 事务二运行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 10 where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set balance = balance + 10 where id = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
产生死锁的必要条件
  1. 互斥条件
  2. 不剥夺条件
  3. 请求和保持条件
  4. 环路等待条件
如何处理死锁
  • 等待,直到超时

    当两个事务开始互相等待,当一个事务等待时间超过设置的阈值innodb_lock_wait_timeout,就将其回滚,另一个事务继续执行,这种方法简单有效。缺点是对于在线事务,这个等待时间也无法接收,但是如果等待时间太短又容易误伤到正常事务

  • 使用死锁检测进行死锁处理

    InnoDB存储引擎提供了wait for graph来主动进行死锁的检测,每当加锁请求无法立即满足需要并进入等待的时候,wait for graph算法就会触发

    • 这是一种主动的死锁检测机制,要求数据库保存锁的信息链表事务等待链表两部分信息,基于这两部分信息,可以绘制wait-for graph等待图。
    image-20230508192812010
    • wait-for graph等待图即以等待事务为顶点,以请求加的锁为边的有向图,如果图中存在环,则说明存在死锁。
    image-20230508192902538
    • 一旦检测到死锁,InnoDB就会选择Undo log量最小的事务,其他事务继续执行(innodb_deadlock_detect=on表示开启这个逻辑)
    • 缺点:每个被堵塞的线程,都要判断是不是由于自己的加入导致了死锁,这个操作(判断有无环,如DFS、BFS算法)的时间复杂度为O(n),则n个节点复杂度为O(n^2)
    • 解决方案:控制并发访问的数量,比如在中间件中实现对于相同行的更新,在进入存储引擎前排队,这样就不会在InnoDB内部有大量的死锁检测工作
    • 进一步的思路:将一行记录改造成逻辑上的多行,这一行记录是其他的加和,这样就避免了对于一行的集中并发访问造成死锁的几率。

4 锁的内存结构与监控策略

4.1 锁的内存结构

​ 了解即可,后续有空再填坑

image-20230508194744389
4.2 锁的监控策略

​ 关于MySQL锁的监控,一般可以通过检查InnoDB_row_lock等状态变量来分析系统上的行锁的争夺情况。

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 349905 | -- 总时长
| Innodb_row_lock_time_avg      | 20582  | -- 平均时长
| Innodb_row_lock_time_max      | 50006  | -- 最大时长
| Innodb_row_lock_waits         | 17     | -- 总次数
+-------------------------------+--------+
5 rows in set (0.02 sec)

其他监控方法:MySQL把事务和锁的信息记录在了information_schema库中,涉及到的三张表分别是INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS

​ MySQL8.0删除了information_schema.INNODB_LOCKS,添加了performance_schema.data_locks用于查看事务的锁的情况,information_schema.INNODB_LOCK_WAITS也被performance_schema.data_lock_wait替代。

posted @ 2023-05-09 13:56  Tod4  阅读(114)  评论(0编辑  收藏  举报