Mysql-锁

by shihang.mai

以下仅对innodb存储引擎,数据库的锁是为了解决事务的隔离性问题

1. 锁介绍

按照类型分类: 共享锁(读锁)、排他锁(写锁)
按照粒度分类: 表锁、行锁,其中行锁又分为记录锁、间隙锁、临键锁
按照状态分类: 意向共享锁、意向排它锁

  • 共享锁(读锁):读并发,写阻塞
  • 排他锁(写锁):只允许一个写,其他阻塞

  • 表锁:锁的粒度大,加锁快,开销小,但是锁冲突的概率大,并发度低
  • 行锁:锁定一行或者多行数据,锁的粒度小,加锁慢,发生锁冲突的概率小,并发度高

  • 记录锁: 锁表中的某一条记录
  • 间隙锁Gap Locks
//Gap Locks会锁住两个索引之间的区间,注意前提是====索引====。即(3,5]区间加锁
select * from User where id>3 and id<5 for update
  • 临键锁Next-Key Locks=Gap Locks + 记录锁
//Next-Key Locks是Gap Locks+行锁形成闭区间锁,即[3,5]区间加锁
select * from User where id>=3 and id=<5 for update

关于意向锁的出现,是因为表锁和行锁会相互冲突的。也不可能加表锁的时候,去遍历整个表的行记录有没加行锁。所以引入了意向锁。这样加表锁的时候,直接判断有没意向锁就可以判断该表有没行锁了

  • 意向共享锁: 一个数据行加共享锁前必须先获得该表的意向共享锁
  • 意向排他锁):事务在给一个数据行加排他锁前必须先获得该表的意向排他锁
    意向锁只会和加表级锁出现冲突的情况

update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。

  • 通过在select语句后显式加lock in share mode加共享锁
  • 通过在select语句后显式加for update来加排它锁

2. 行锁实现方式

对索引加锁实现

3. 加锁判断

对锁的影响.png

4.1 隔离等级对锁的影响

在分析具体加锁场景时,首先要确定当前的隔离等级

  • 读未提交(Read Uncommitted 后续简称 RU):是没有加任何锁的。所以它性能最好
  • 读已提交(Read Committed 后续简称 RC):用MVCC提高并发性能。存在幻读问题,对当前读获取的数据加记录锁。
  • 可重复读(Repeatable Read 后续简称 RR):用MVCC提高并发性能。从传统的隔离级别来看,是会有幻读的,只是mysql用间隙锁解决了。对当前读获取的数据加记录锁,同时对涉及的范围加间隙锁。
  • 序列化(Serializable):基于锁的并发控制,加锁的粒度大,读的时候加共享锁,不能写;写的时候,加的是排它锁,阻塞其它事务的写入和读取,不存在快照读,都是当前读,所以它的性能最差。

4.2 sql语句对锁的影响

  • SELECT ... 语句正常情况下为快照读,不加锁;
  • SELECT ... LOCK IN SHARE MODE 语句为当前读,加 S 锁;
  • SELECT ... FOR UPDATE 语句为当前读,加 X 锁;
  • 常见的 DML 语句(如 INSERT、DELETE、UPDATE)为当前读,加 X 锁;
  • 其中,当前读的 SQL 语句的 where 从句的不同也会影响加锁,包括是否使用索引,索引是否是唯一索引等等。具体情况具体分析。

4.3 当前数据对锁的影响

如一条最简单的根据主键进行更新的 SQL 语句,如果主键存在,则只需要对其加记录锁,如果不存在,则需要再加间隙锁。还有很多很多。

5. 实践

建立表+数据

DROP TABLE IF EXISTS `msh_test`;
CREATE TABLE `msh_test` (
  `id` bigint(11) NOT NULL COMMENT '主键',
  `isbn` varchar(50) DEFAULT NULL COMMENT '书号',
  `author` varchar(50) DEFAULT NULL COMMENT '作者',
  `score` decimal(5,2) DEFAULT NULL COMMENT '评分',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_isbn` (`isbn`) USING BTREE COMMENT '唯一索引',
  KEY `idx_author` (`author`) USING BTREE COMMENT '非唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `msh_test` VALUES (1, 'N0001', 'msh', 3.40);
INSERT INTO `msh_test` VALUES (2, 'N0002', 'bob', 7.60);
INSERT INTO `msh_test` VALUES (3, 'N0003', 'alic', 5.43);
INSERT INTO `msh_test` VALUES (4, 'N0004', 'yoko', 2.32);
INSERT INTO `msh_test` VALUES (5, 'N0005', 'tom', 2.41);
INSERT INTO `msh_test` VALUES (6, 'N0006', 'yui', 9.80);
INSERT INTO `msh_test` VALUES (10, 'N0010', 'tgb', 5.34);

可能用到的sql语句

-- 设置是否自动提交事务 1:开启 0:关闭
set session autocommit=1;
-- 查看当前会话是否自动提交事务
show session variables like 'autocommit';
-- 查看当前会话事务隔离级别
SELECT @@SESSION.transaction_isolation
-- 设置当前会话事务隔离级别
set session transaction isolation level REPEATABLE READ;
set session transaction isolation level READ COMMITTED;
set session transaction isolation level READ UNCOMMITTED;
set session transaction isolation level SERIALIZABLE;

加锁的规则较为复杂。具体情况具体分析

6. 用sql实现乐观锁和悲观锁

MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意

悲观锁

  1. 悲观锁比较适合并发量较小又需要独占读取结果并依赖读取的结果进行判断的业务场景

  2. 实现:

    select * from table where id = 1 for update;
    

乐观锁

  1. 乐观锁比较适合并发量不高,并且写操作不频繁的场景

  2. 实现:

    • 类似于CAS:数据库中添加版本字段,先查询出当前version,更新时带上oldVersion

      update table set columnA = 1,version=version+1 where id=#{id} and version = #{oldVersion}
      
    • CAS提交的时候检测版本有没有改变,只要有变化都会失败,而有一类场景当字段只需要满足一个区间范围并不关心是否有数据更新冲突,且本身进行更新并且作为判断条件时,可不借助其他字段,对字段本身作判断即可.例如一个较常见的场景:库存的扣减,只要扣减后的值大于等于零即可

      update product set rest = rest– #{deduct} where name = ‘abc’ and rest = #{deduct}
      

参考

https://zhuanlan.zhihu.com/p/149228460
https://zhuanlan.zhihu.com/p/149228460
https://www.cnblogs.com/crazylqy/p/7611069.html

你可能感兴趣的:(Mysql-锁)