今天是个好天气 logo 今天是个好天气
  • Home
  • Go
  • MySQL
  • Redis
  • LeetCode
  • Hello World
↩️README Hello MySQL SQL 写点SQL InnoDB存储引擎 索引 事务 日志 有哪些锁 MySQL如何加锁 MySQL性能如何优化

MySQL如何加锁

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

什么SQL语句会加行级锁

普通的 select 语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,是通过 MVCC(多版本并发控制)实现的。

可以手动添加:

//对读取的记录加共享锁(S型锁)
select ... lock in share mode;

//对读取的记录加独占锁(X型锁)
select ... for update;

当事务提交时,锁会被释放,所以在使用这两条语句时,需要加上begin或者start transaction。

update 和 delete 操作都会加行级锁,且锁的类型都是独占锁(X型锁)。

//对操作的记录加独占锁(X型锁)
update table .... where id = 1;

//对操作的记录加独占锁(X型锁)
delete from table where id = 1;

MySQL是怎么加行级锁的

加锁的对象是索引,加锁的基本单位是 next-key lock。

next-key lock 在一些场景下会退化成记录锁或间隙锁:在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。

唯一索引等值查询

用唯一索引进行等值查询的时候,查询的记录存不存在,加锁的规则也会不同:

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。
  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。

为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成记录锁?

在唯一索引等值查询并且查询记录存在的场景下,仅靠记录锁也能避免幻读的问题。

幻读是一个事务前后两次查询到的结果集不同。避免幻读就是避免结果集一条记录被其他事务删除或者插入一条新的记录。

  • 由于主键唯一性,其他事务插入查询id记录,比如id = 1的记录时,会因为主键冲突,导致无法查询id = 1的新记录。
  • 加了记录锁(X锁)后,其他事务无法对记录进行增删改。

为什么唯一索引等值查询并且查询记录「不存在」的场景下,在索引树找到第一条大于该查询记录的记录后,要将该记录的索引中的 next-key lock 会退化成「间隙锁」?

在唯一索引等值查询并且查询记录不存在的场景下,仅靠间隙锁就能避免幻读的问题。

  • next-key锁会影响其他记录的删除,其他记录的删除不会影响当前查询(不存在)的结果,只需要Gap锁来避免插入影响结果集的新纪录即可。
  • 锁是加在索引上的,当查询记录不存在时,无法锁住不存在的记录。

唯一索引查询范围

当唯一索引进行范围查询时,**会对每一个扫描到的索引加 next-key 锁,**然后如果遇到下面这些情况,会退化成记录锁或者间隙锁:

  • 情况一:针对「大于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。

对于等值部分同上面说明的,主键唯一性避免了插入,记录锁又避免了插入新记录,避免了幻读。

对于其他部分,增加next-key锁避免删除更改、新增记录。


  • 情况二:针对「小于或者小于等于」的范围查询,要看条件值的记录是否存在于表中:

    • 当条件值的记录不在表中,那么不管是「小于」还是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。

    因为这里的终止范围不是符合条件的记录,没必要加记录锁。

    • 当条件值的记录「在」表中时:
      • 如果是「小于」条件的范围查询,扫描到终止范围查询的记录时,该记录的主键索引中的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的主键索引上,加 next-key 锁。
      • 如果是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的主键索引中加的都是 next-key 锁

非唯一索引等值查询

当我们用非唯一索引进行等值查询的时候,因为存在两个索引,一个是主键索引,一个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁。

  • 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描

    • 然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁

    • 对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁

    这里避免了同age而主键id靠后,逃过Next-key锁后的插入,避免幻读现象,

    • 同时,在符合查询条件的记录的主键索引上加记录锁

  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。

由于间隙锁是左开右开的,对于边界上的位置能否插入新纪录,根据**「二级索引值(age列)+主键值(id列)」**同时判断插入位置后面是否具有Gap锁。

所以也可以看到LOCK_DATA:39,20,增加一个字段来判断哪些范围的id值可以插入。

非唯一索引范围查询

非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。

因为在二级索引中字段不是唯一的,如果只加记录锁,无法防止插入或者修改,出现幻读。

没有加索引的查询

如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。

在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

锁结构

在线修改表结构

  • 在业务系统运行过程中随意删改字段,会造成重大事故。
    • 常规的做法是:业务停机,再 维护表结构,比如:12306 凌晨 0 点到早上 7 点是停机维护。
  • 如果是不影响正常业务的表结构是允许在线修改的。
    • 比如:int 类型不够用了,要缓存 bigint、有唯一性约束,要去掉。这不会影响线上的正在执行的数据

alter table弊端

  • 表级锁

    修改表结构会锁表,因此在修改表结构时,影响表的写入操作;

    数据越多,锁表时间越长。

  • 修改失败,还原表结构,耗时长

    如果修改表结果失败,必须还原表结构,所以耗时更长;

    比如:添加一个唯一性约束,结果发现很多数据有控制,无法添加进来了,这个时候就只能还原表结构

  • 大数据表记录多,修改表结构锁表时间很久、

使用PerconaTookit

由于 alter table 线上修改表结构有诸多弊端,但是 PerconaTookit 提供了一个开源的线上修改表结构的工具。

其中一个名为 pt-online-schema-change 的工具可以完成在线修改表结构。

PerconaTookit原理

  1. 复制一份 order 表结构
  2. 在这个新表的修改表结构
  3. 同步执行数据拷贝
    • 修改完成之后,会在原来表上增加触发器,新的操作数据增删改查都会同步到新的表中,
    • 同时会把原来表的数据拷贝到新表中。
    • 当数据拷贝完之后,且原表没有新的数据写入时,把原表删除,把新表名称修改为原表名称

死锁

死锁的发生

普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以显式使用:

begin;
//对读取的记录加共享锁
select ... lock in share mode;
commit; //锁释放

begin;
//对读取的记录加排他锁
select ... for update;
commit; //锁释放

行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。

如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行记录加上了行锁,还给行记录两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。

在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞。

如何避免死锁

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

  • 设置事务等待锁的超时时间。

    • 设置锁等待超时参数:innodb_lock_wait_timeout,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
  • 开启主动死锁检测。

当一个事务的等待时间超过该值或者主动死锁检测到死锁后,主动回滚到链条中某一个十五,让其他事务正常运行,


  • 对索引加锁顺序的不一致很可能会导致死锁,所以如果可以,尽量以相同的顺序来访问索引记录和表。在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能;
  • Gap 锁往往是程序中导致死锁的真凶,由于默认情况下 MySQL 的隔离级别是 RR,所以如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,可以避免 Gap 锁导致的死锁;
  • 为表添加合理的索引,如果不走索引将会为表的每一行记录加锁,死锁的概率就会大大增大;
  • 我们知道 MyISAM 只支持表锁,它采用一次封锁技术来保证事务之间不会发生死锁,所以,我们也可以使用同样的思想,在事务中一次锁定所需要的所有资源,减少死锁概率;
  • 避免大事务,尽量将大事务拆成多个小事务来处理;因为大事务占用资源多,耗时长,与其他事务冲突的概率也会变高;
  • 避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操作数据量比较大的语句;我们经常会有一些定时脚本,避免它们在同一时间点运行;

如何排查死锁

一般情况是由于批量更新时加锁顺序不一致而导致的死锁。

  1. 阅读死锁日志。

悲观锁与乐观锁

是什么

悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程 A,其他线程就必须等待该线程 A 处理完才可以处理。

数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及 syncronized 实现的锁均为悲观锁。


乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。

如何添加

首先要关闭MySQL的关于每一条SQL的自动提交,MySQL 默认使用 autocommit 模式。

悲观锁加锁的SQL语句:select num from t_goods where id = 2 for update。

  • 加锁失败,说明数据正在被其他事务修改,当前查询返回等待或者抛出异常。
  • 加锁成功,可以对事务修改,事务完成后就会解锁。在事务提交前,阻塞其他事务的查询。

乐观锁使用version来实现,当不同事务提交修改时,查看version是否变更,有变更会在提交时修改失败。

Compare-and-Swap(CAS)

比较并置换,有时候也叫Compare and Set,比较并设置。

1、比较:读取到了一个值 A,在将其更新为 B 之前,检查原值是否仍为 A(未被其他线程改动)。

2、设置:如果是,将 A 更新为 B,结束。[1]如果不是,则什么都不做。

上面的两步操作是原子性的,可以简单地理解为瞬间完成,在 CPU 看来就是一步操作。有了 CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS 利用 CPU 指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。

ABA

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

解决办法:

我们需要加上一个版本号(Version),在每次提交的时候将版本号+1 操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息。

对比

  • 悲观锁阻塞事务,乐观锁回滚重试。
  • 乐观锁适用于写比较少的情况,冲突很少发生,节省锁的开销,加大系统的整个吞吐量。
  • 如果经常产生冲突,上层应用会不断进行重试,反而降低了性能,所以这种情况下用悲观锁比较合适。
  1. 什么SQL语句会加行级锁
  2. MySQL是怎么加行级锁的
    1. 唯一索引等值查询
    2. 唯一索引查询范围
    3. 非唯一索引等值查询
    4. 非唯一索引范围查询
    5. 没有加索引的查询
  3. 锁结构
  4. 在线修改表结构
    1. alter table弊端
    2. 使用PerconaTookit
    3. PerconaTookit原理
  5. 死锁
    1. 死锁的发生
    2. 如何避免死锁
    3. 如何排查死锁
  6. 悲观锁与乐观锁
    1. 是什么
    2. 如何添加
    3. Compare-and-Swap(CAS)
    4. ABA
    5. 对比
Created by shixiaocaia | Powered by idoc
Think less and do more.