MySQL 事务与锁详解

一、什么是事务

1.1 事务的定义

维基百科的定义:

事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由 一个有限的数据库操作序列构成。

这里面有两个关键点:

  • 第一个,所谓的逻辑单位,意味着它是数据库最小的工作单 元,是不可以再分的
  • 第二个,它可能包含了一个或者一系列的 DML 语句,包括 insert delete update

1.2 哪些存储引擎支持事务

mysql 中的 InnoDB

1.3 事务的四大特性

1)原子性(Atomicity):意味着我们对数据库的一系列的操作,要么都是成功, 要么都是失败,不可能出现部分成功或者部分失败的情况。

以转账的场景为例,一个账户的余额减少,必然对应着另一个账户余额的增加。全部成功比较简单,问题是如果前面一个操作已经成功了,后面的操作失败了,怎 么让它全部失败呢?这个时候我们必须要回滚。

原子性,在InnoDB里面是通过 undo log来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作。

2)隔离性(Isolation):有了事务的定义以后,在数据库里面会有很多的事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作。 我们对隔离性的定义,就是这些很多个的事务,对表或者行的并发操作,应该是透明的, 互相不干扰的。

比如两个人给我转账100,开启两个事务,都拿到了我账户的余额 1000,然后各自基于1000加100,最后结果是1100,就出现了数据混乱的问题。

3)持久性(Durability):操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为数据库掉电、 宕机、意外重启,又变成原来的状态。这个就是事务的持久性。

持久性怎么实现呢?InnoDB崩溃恢复crash-safe是通过什么实现的?

持久性是通过redo logdouble write buffer (双写缓冲)来实现的,我们操作数据的时候,会先写到内存的buffer pool里面,同时记录redo log,如果在刷盘之前出现异常,在重启后就可以读取redo log的内容,写入到磁盘,保证数据的持久性。

当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲保证。

需要注意的是,原子性,隔离性,持久性,最后都是为了实现一致性。

4)读一致性(Consistency)

1.4 数据库什么时候会出现事务

增删改的语句会自动开启事务,当然是一条SQL 一 个事务。注意每个事务都是有编号的,这个编号是一个整数,有递增的特性。

如果要把多条SQL放在一个事务里面,就要手动开启事务。

手动开启事务有两种方 式:

  • 一种是用 begin;
  • —种是用 start transactiono

那么怎么结束一个事务呢?结束也有两种方式:

  • 第一种是回滚事务rollback,事务 结束。
  • 第二种就是提交一个事务,commit,事务结束。

InnoDB里面有一个autocommit的参数(分为两个级别,session级别和global 级别)。

show variables like 'autocommit';

它的默认值是ON。autocommit这个参数是什么意思呢?是否自动提交。如果它的 值是true/on的话,我们在操作数据的时候,会自动提交事务。

否则的话,如果我们把autocommit设置成false/off,那么数据库的事务就需要我 们手动地结束,用rollback或者commit

还有一种情况,客户端的连接断开的时候,事务也会结束。

1.5 事务并发会带来的问题

问题一:读到未提交的事务,脏读

问题二:同一个事务读到不同的数据(update/delete),不可重复读

问题三:同一个事务读到不同的数据(insert),幻读

总结:事务并发的三大问题其实都是数据读一致性问题,必须由数据库提供一定的事务隔离机制来解决。

1.6 SQL92 标准

所以,美国国家标准协会(ANSI)制定了一个SQL标准,也就是说建议数据库厂商

都按照这个标准,提供一定的事务隔离级别,来解决事务并发的问题。这个SQL标准有 很多的版本,大家最熟悉的是SQL92标准。

我们来看一下 SQL92 标准的官网。

MySQL 事务与锁详解_第1张图片

这里面有一张表格,里面定义了四个隔离级别,右边的 P1 P2 P3 就是代表事务并发的3个问题,脏读,不可重复读,幻读。Possible代表在这个隔离级别下,

这个问题有可能发生,换句话说,没有解决这个问题。Not Possible就是解决了这个问题。

  • Read Uncommitted (未提交读):一个事务可以读取到其 他事务未提交的数据,会出现脏读,所以叫做 RU,它没有解决任的问题。
  • Read Committed (已提交读):也就是一个事务只能读取 到其他事务已提交的数据,不能读取到其他事务未提交的数据,它解决了脏读的问题, 但是会出现不可重复读的问题。下面简称 RC
  • Repeatable Read (可重复读):它解决了不可重复读的问题, 也就是在同一个事务里面多次读取同样的数据结果是一样的,但是在这个级别下,没有 定义解决幻读的问题。下面简称 RR
  • Serializable (串行化):在这个隔离级别里面,所有的事务都是串 行执行的,也就是对数据的操作需要排队,已经不存在事务的并发操作了,所以它解决 了所有的问题。

1.7 MySQL InnoDB对隔离级别的支持

MySQL 事务与锁详解_第2张图片

InnoDB 支持的四个隔离级别和 SQL92定义的完全一致,隔离级别越高,事务的并 发度就越低。唯一的区别就在于,InnoDB 在 RR 的级别就解决了幻读的问题。

也就是说,不需要使用串行化的隔离级别去解决所有问题,既保证了数据的一致性,又支持较高的并发度。

这个就是 InnoDB 默认使用 RR 作为事务隔离级别的原因。

1.8 两大实现方案

如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?

总体上来说,我们有两大类的方案。

1.8.1 LBCC

第一种,既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操 作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制Lock Based Concurrency Control (LBCC)

如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那 就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地 影响操作数据的效率。

1.8.2 MVCC

所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的之前给它建立一个备份或者叫快照,后面再来读取这个快照 就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control (MVCC)

MVCC的原则:

一个事务能看到的数据版本:

  • 第一次查询之前已经提交的事务的修改
  • 本事务的修改

一个事务不能看见的数据版本:

  • 在本事务第一次查询之后创建的事务(事务ID比我的事务ID大)
  • 活跃的(未提交的)事务的修改

MVCC 的效果:我可以査到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的。

所以我们才把这个叫做快照,不管别的事务做任何增删改查的操作,它只能看到第 一次查询时看到的数据版本。

我们有一个数据结构,把本事务心、活跃事务ID、当前系统最大事务ID存起来,这样才能实现判断。这个数据结构就叫 Read View(可见性视图),每个事务都维护一个自己的Read View

MySQL 事务与锁详解_第3张图片

  • m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务id列表。
  • min trx id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务id,也就是m ids中的最小值。
  • max trx id:表示生成 ReadView 时系统中应该分配给下一个事务的id值。
  • creatortrxid:表示生成该 ReadView 的事务的事务id。

有了这个数据结构以后,事务判断可见性的规则是这样的:

​ 0、从数据的最早版本开始判断(undo log)

​ 1、 数据版本的 trx_id = creator trx id,本事务修改’可以访问

​ 2、 数据版本的 trx_id < min_trx_id (未提交事务的最小 ID ),说明这个版本在 生成 ReadView 已经提交,可以访问

​ 3、 数据版本的 trx id > max_trx_id (下一个事务 ID ),这个版本是生成 ReadView 之后才开启的事务建立的,不能访问

​ 4、 数据版本的 trx id在min trx id 和 max_trx_id 之间,看看是否在 m ids 中。 如果在,不可以。如果不在,可以。

​ 5、 如果当前版本不可见,就找 undo log 链中的下一个版本。

注意:RR 中 Read View 是事务第一次査询的时候建立的。RC 的 Read View 是事务每次 查询的时候建立的。

需要注意,在InnoDB中,MVCC 和锁是协同使用的,这两种方案并不是互斥的。

第一大类解决方案是锁,锁又是怎么实现读一致性的呢?

二、MySQL InnoDB 锁的基本基本类型

2.1 锁的粒度

InnoDB 和 MylSAM 支持的锁 的类型是不同的。MylSAM 只支持表锁,而 InnoDB 同时支持表锁和行锁。

表锁和行锁的区别:

  • 锁定粒度:表锁 > 行锁
  • 加锁效率:表锁 > 行锁
  • 冲突概率:表锁 > 行锁
  • 并发性能:表锁 < 行锁

2.2 锁的类型

MySQL 事务与锁详解_第4张图片

我们可以看到,官网把锁分成了 8类。我们把前面的两个行级別的锁Shared and Exclusive Locks),和两个表级别的锁Intention Locks称为锁的基本模式。

后面三个Record LocksGap LocksNext-Key Locks,我们把它们叫做锁的算法, 也就是分别在什么情况下锁定什么范围。

插入意向锁:是一个特殊的间隙锁。间隙锁不允许插入数据,但是插入意向锁允许 多个事务同时插入数据到同一个范围。比如(4,7),—个事务插入5, —个事务插入6,不 会发生锁等待。

**自增锁:**是一种特殊的表锁,用来防止自增字段重复,数据插入以后就会释放,不 需要等到事务提交才释放。如果需要选择更快的自增值生成速度或者更加连续的自增值, 就要通过修改自增锁的模式改变。

show variables like Innodb autoinc lock mode1;
  1. traditonal (每次都会产生表锁)
  2. consecutive (会产生一个轻量锁,simple insert会获得批量的锁,保证连续插入,默认值)
  3. interleaved (不会锁表,来一个处理一个,并发最高)

Predicate Locks for Spatial Indexes 是5.7版本里面新增的一种数据类型的索引的 锁,

2.3 共享锁

第一个行级别的锁就是我们在官网看到的Shared Locks (共享锁),我们获取了一 行数据的读锁以后,可以用来读取数据,所以它也叫做读锁,注意不要在加上了读锁以 后去写数据,不然的话可能会出现死锁的情况。而且多个事务可以共享一把读锁。

共享锁的作用:因为共享锁会阻塞其他事务的修改,所以可以用在不允许其他事务 修改数据的情况

--- 手工加上一把读锁
select ............. ..... lock in share mode;

释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。

2.4 排它锁

二个行级别的锁叫做Exclusive Locks (排它锁),它是用来操作数据的,所以又 叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数 据的共享锁和排它锁。

排它锁的加锁方式有两种:

第一种是自动加排他锁,可能是同学们没有注意到的: 我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。

还有一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁,这个 无论是在我们的代码里面还是操作数据的工具里面,都比较常用。

释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。释放锁的方式跟前面是一样的。

2.5 意向锁

意向锁是什么呢?我们好像从来没有听过,也从来没有使用过,其实他们是由数据库自己维护的。

也就是说:

  • 当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个 意向共享锁。
  • 当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁。

反过来:

  • 如果一张表上面至少有一个意向共享锁*,*说明有其他的事务给其中的某些数据行加上了共享锁。
  • 如果一张表上面至少有一个意向排他锁,说明有其他的事务给其中的某些数据行加 上了排他锁。

意向锁跟意向锁是不冲突的,意向锁跟行锁也不冲突。

那么这两个表级别的锁存在的意义是什么呢?

如果说没有意向锁的话,当我们准备给一张表加上表锁的时候,我们首先要做什么? 是不是必须先要去判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加 上表锁。那么这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果 数据量特别大,比如有上千万的数据的时候,加表锁的效率特別低。

但是我们引入了意向锁之后就不一样了。我只要判断这张表上面有没有意向锁,如 果有,就直接返回失败。如果没有,就可以加锁成功。所以 InnoDB 里面的表锁,我们 可以把它理解成一个标志。就像火车上卫生间有没有人使用的灯,让你不用去推门,是 用来提高加锁的效率的。

锁是用来解决事务对数据的并发访问的问题的。

三、行锁的原理

InnoDB 的行锁,就是通过锁住索 引来实现的。这个时候问题就来了,

1、为什么表里面没有索弓I的时候,锁住一行数据会导致锁表?或者说,如果锁住的是索引,一张表没有索引怎么办?所以,一张表有没有可能没有索引?

  • 如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
  • 如果没有显式定义主键,则 InnoDB 会选择第一个不包含有NULL值的唯一索 引作为主键索引。
  • 如果也没有这样的唯一索引,则 InnoDB 会选择内置6字节长的 ROWID作 为隐藏的聚集索引,它会随着行记录的写入而主键递增。

所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索弓I都锁住了。

2、为什么通过唯一索引给数据行加锁,主键索引也会被锁住?

唯一索引是属于辅助索引。

主键索引里面除了索引之外,还存储了完整的数据。所以我们通过辅助索引锁定 一行数据的时候,它跟我们检索数据的步骤是一样的,会通过主键值找到主键索引,然 后也锁定。

本质上是因为锁定的是同一行数据,是相互冲突的。

MySQL 事务与锁详解_第5张图片

主键索引:存储索引和数据

辅助索引:存储索引和主键值

四、锁的算法

假设 test 这张表有一个主键索引,前面我们已经见过了。

我们插入了 4行数据,主键id分别是1、4、7、*10。

因为我们用主键索弓I加锁,我们这里的划分标准就是主键索引的值。

MySQL 事务与锁详解_第6张图片

​ 1)这些数据库里面存在的主键值,我们把它叫做 Record,记录,那么这里我们就有4个 Record

​ 2)根据主键,这些存在的 Record 隔开的数据不存在的区间,我们把它叫做 Gap 间隙,它是一个左开右开的区间。

​ 假设我们有 N 个 Record,那么所有的数据会被划分成多少个 Gap 区间?

​ 答案是N +1,就像我们把一条绳子砍N刀,它最后肯定是变成N +1段。

​ 3)最后一个,间隙(Gap)连同它左边的记录(Record),我们把它叫做临键的区间,它是一个左开右闭的区间。再重复一次,是左开右闭。

整型的主键索引,它是可以排序,所以才有这种区间。如果我的主键索引不是整形,是字符怎么办呢?

任何一个字符集,都有相对应的排序规则:

在这里插入图片描述

4.1 记录锁(Record Lock)

第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,

精准匹配到一条记录的时候,这个时候使用的就是记录锁。

4.2 间隙锁(Gap Lock)

第二种情况,当我们査询的记录不存在,没有命中任何一个 record ,无论是用等值

査询还是范围查询的时候,它使用的都是间隙锁。

重复一遍,当査询的记录不存在的时候,使用间隙锁。

注意,间隙锁主要是阻塞插入 inserto 相同的间隙锁之间不冲突。

4.3 临键锁 (Next-key Lock)

第三种情况,当我们使用了范围査询,不仅仅命中了 Record记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。

唯一性索引,等值査询匹配到一条记录的时候,退化成记录锁。

没有匹配到任何记录的时候,退化成间隙锁。

Next-key Lock = Gap Lock + Record Lock

临键锁,锁住最后一个 key 的下一个左开右闭的区间。解决了幻读的问题。

4.4 小结:隔离级别的实现

为什么InnoDB的RR级别能够解决幻读的 问题,就是用临键锁实现的。

再回过头来看下这张图片,这个就是MySQL InnoDB里面事务隔离级别的实现。

MySQL 事务与锁详解_第7张图片

最后我们来总结一下四个事务隔离级别的实现:

4.4.1 :Read Uncommitted

​ RU隔离级别:不加锁。

4.4.2 :Serializable

​ Serializable 所有的 select 语句都会被隐式的转化为 select… in share mode ,会和 updates delete 互斥。

这两个很好理解,主要是RR和RC的区别

4.4.3 :Repeatable Read

​ RR隔离级别下,普通的 select 使用快照读(snapshot read),底层使用 MVCC 来实现。

​ 加锁的 select(select … in share mode / select … for update) 以及更新操作 update, delete等语句使用当前读(current read),底层使用记录锁、或者间隙锁、 临键锁。

4.4.4 :Read Commited

​ RC 隔离级别下,普通的 select 都是快照读,使用 MVCC 实现。 加锁的select都使用记录锁,因为没有 Gap Lock。

​ 除了两种特殊情况:外键约束检査( foreign-key constraint checking )以及重复键检査(duplicate-key checking)时会使用间隙锁封锁区间。

​ 所以RC会出现幻读的问题。

五、事务隔离级别怎么选

RU 和 Serializable 肯定不能用。有些公司要用 RC 呢?

RC 和 RR 主要有几个区别:

  • RR的间隙锁会导致锁定范围的扩大。
  • 条件列未使用到索引, RR锁表,RC锁行。
  • RC 的 "半一致性” (semi-consistent)读可以增加 update 操作的并发性。

在 RC 中,一个 update 语句,如果读到一行已经加锁的记录,此时 InnoDB 返回记 录最近提交的版本,由 MySQL 上层判断此版本是否满足 update 的 where 条件。若满足(需要更新),则 MySQL 会重新发起一次读操作,此时会读取行的最新版本(并加锁)。

实际上,如果能够正确地使用锁(避免不使用索引去枷锁),只锁定需要的数据,用默认的RR级别就可以了。

在我们使用锁的时候,有一个问题是需要注意和避免的,我们知道,排它锁有互斥的特性。一个事务或者说一个线程持有锁的时候,会阻止其他的线程获取锁,这个时候 会造成阻塞等待,如果循环等待,会有可能造成死锁。

六、死锁

6.1 锁的释放与阻塞

锁的释放:

  • 事务结束(commit,rollback)
  • 客户端连接断开

如果一个事务一直未释放锁,其他事务会被阻塞 50 秒。MySQL有一个参数来控制获取锁的等待时间,默认是50秒。

show VARIABLES like 'Innodb_lock_wait_timeout';

在这里插入图片描述

对于死锁,是无论等多久都不能获取到锁的,这种情况,也需要等待50秒钟吗?那 不是白白浪费了 50秒钟的时间吗?

我们先来看一下什么时候会发生死锁。

6.2 死锁的发生和检测

死锁演示: 案例1

Session 1 Session 2
begin; select * from t2 where id =1 for update;
begin; delete from t2 where id =4 ;
update t2 set name= ‘4d’ where id =4 ;
delete from t2 where id =1;

案例2

Session 1 Session 2
begin; select * from tl where id =1 lock in share mode;
begin; select * from tl where id =1 lock in share mode;
update tl set name= ‘la’ where id =1;
update tl set name= ‘la’ where id =1;

我们看到:在第一个事务中,检测到了死锁,马上退岀了,第二个事务获得了锁, 不需要等待50秒:

[Err] 1213 ・ Deadlock found when trying to get lock; try restarting transaction

为什么可以直接检测到呢?是因为死锁的发生需要满足一定的条件,所以在发生死锁时,InnoDB —般都能通过算法wait-for graph自动检测到。

那么死锁需要满足什么条件?死锁的产生条件,因为锁本身是互斥的:

  • 同一时刻只能有一个事务持有这把锁;
  • 其他的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺;
  • 当多个事务形成等待环路的时候,即发生死锁。

理发店有两个总监。一个负责剪头的 Tony 老师,一个负责洗头的 Kelvin 老师。 Tony老师不能同时给两个人剪头,这个就叫互斥

Tony 在给别人在剪头的时候,你不能让他停下来帮你剪头,这个叫不能强行剥夺

如果 Tony 的客户对 Kelvin 说:你不帮我洗头我怎么剪头? Kelvin 的客户对 Tony 说:你不帮我剪头我怎么洗头?这个就叫形成等待环路

实际上,发生死锁的情况非常多,但是都满足以上3个条件。

这个也是表锁是不会发生死锁的原因*,*因为表锁的资源都是一次性获取的。

如果锁一直没有释放,就有可能造成大量阻塞或者发生死锁,造成系统吞吐量下降, 这时候就要查看是哪些事务持有了锁。

6.3 查看锁信息(日志)

首先,SHOW STATUS命令中,包括了一些行锁的信息:

show status like 'innodb row lock';

结果如下:

MySQL 事务与锁详解_第8张图片

  • lnnodb_row_lock_current_waits:当前正在等待锁定的数量;
  • lnnodb_row_lock_time:从系统启动到现在锁定的总时间长度,单位ms;
  • lnnodb_row_lock_time_avg:每次等待所花平均时间;
  • lnnodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间;
  • lnnodb_row_lock_waits:从系统启动到现在总共等待的次数。

SHOW命令是一个概要信息。InnoDB 还提供了三张表来分析事务与锁的情况:

--当前运行的所有事务,还有具体的语句
select * from information schema.INNODB TRX;

在这里插入图片描述

---当前出现的锁
select * from information schema.INNODB LOCKS; 

在这里插入图片描述

---锁等待的对应关系
selectfrom information schema.INNODB LOCK WAITS;

在这里插入图片描述

更加详细的锁信息,开启标准监控和锁监控:

set GLOBAL innodb_status_output=ON; 
set GLOBAL innodb_status_output_locks=ON;

通过分析锁日志,找出持有锁的事务之后。如果一个事务长时间持有锁不释放,可以kill事务对应的线程 ID ,也就是INNODB TRX表中的trx_mysqI_thread_id,例如执行kill 4, kill 7, kill 8

当然,死锁的问题不能每次都靠kill线程来解决,这是治标不治本的行为。我们应该尽量在应用端,也就是在编码过程中避免。

6.4 死锁的避免

  1. 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路)
  2. 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路)
  3. 申请足够级别的锁,如果要操作数据,就申请排它锁
  4. 尽量使用索引访问数据,避免没有 where 条件的操作(避免锁表)
  5. 如果可以,大事务化成小事务
    转存中…(img-lyUY5QLj-1648323536736)]

更加详细的锁信息,开启标准监控和锁监控:

set GLOBAL innodb_status_output=ON; 
set GLOBAL innodb_status_output_locks=ON;

通过分析锁日志,找出持有锁的事务之后。如果一个事务长时间持有锁不释放,可以kill事务对应的线程 ID ,也就是INNODB TRX表中的trx_mysqI_thread_id,例如执行kill 4, kill 7, kill 8

当然,死锁的问题不能每次都靠kill线程来解决,这是治标不治本的行为。我们应该尽量在应用端,也就是在编码过程中避免。

6.4 死锁的避免

  1. 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路)
  2. 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路)
  3. 申请足够级别的锁,如果要操作数据,就申请排它锁
  4. 尽量使用索引访问数据,避免没有 where 条件的操作(避免锁表)
  5. 如果可以,大事务化成小事务
  6. 使用等值查询而不是范围查询查询数据,命中记录(避免间隙锁对并发的影响)

你可能感兴趣的:(MySQL,redis,分布式锁,缓存)