MySQL学习之锁


数据库锁的设计初衷是为了解决并发问题。数据库作为一个多用户共享的资源,当出现并发访问时就需要数据库合理的控制访问规则,而锁就是来实现访问规则的一直方式

根据锁的范围划分,在MySQL中可以分为全局锁、表级锁和行锁三类

1.全局锁

全局锁顾名思义就是对整个数据库实例加锁,全局锁的典型使用场景就是做全局的逻辑备份,备份过程中整个库处于只读状态

使整个库处于只读状态可以执行命令flush tables with read lock(FTWRL),执行这个命令后其他线程执行数据更新语句(数据的增删改)数据定义语句(包括建表、修改表结构) 时都会被阻塞

另一种方式是使用官方自带的逻辑备份工具mysqldump使用-- single transacation参数,原理是备份前开启一个事务拿到一致性视图,根据事务的隔离级别,在可重复读级别下这个视图中读的数据总是与开启事务时一致。single transacation只适用于所有表都使用的事务引擎的库

2.表级锁

在MySQL里表级锁有表锁和元数据锁(meta data lock)MDL

2.1.表锁

表锁的语法是:

-- 加锁
lock tables tableName1 read/write,tableName2 read/write ... ;
-- 释放锁
unlock tables

举一个例子,如果一个线程A执行lock tables t1 read,t2 write之后,其他线程只能读t1,写t1和读写t2都会被阻塞。A线程也只能读t1和读写t2,其他表不能访问

当使用lock tables控制并发时锁住了整个表,从而带来了严重的性能问题,InnoDB支持行锁一般就不使用lock tables控制并发了,将lock tables和unlock tables改成begin和commit

2.2.元数据锁

MDL是在访问表时自动加上,不需要显示指定。MDL的作用是保证数据读写的正确性。当一个线程查询出一个列表数据时,同时另一个线程删除了一个字段,这样造成数据不一致显然这种情况时不能接受的

当对一个表进行增删改时加MDL读锁,当改变表结构时加MDL写锁

  • MDL读锁之间不互斥,多个线程可以对同一张表做增删改查操作
  • MDL写锁和读写锁之间是互斥的,用来保证变更表结构时数据操作的安全性。当两个线程都需要变跟表结构时一个线程必须要等待另一个线程执行变更完之后能才能执行

当我们需要跟一个表添加字段时,首先要解决长事务,因为事务会一直占着MDL锁直到事务提交才会释放,当这个表是热点表又需要在这个表上加字段,比较理想的机制是在执行alter table语句里增加等待时间,MariaDB引擎已经实现

alter table t1 nowait add column ...
alter table t2 wait add column ...

3.行锁

顾明思意行锁就是数据表中行的锁,当两个事务A,B同时更新同一行时,只能等事务A执行完之后事务B才能去执行更新操作

InnoDB的行锁是针对索引加的锁,不是针对记录的锁,并且索引不能失效,否则会从行锁升级成表锁

  • 行锁的优势:锁的粒度小,发生锁冲突的概率低,处理并发能力强
  • 行锁的劣势:开销大,加锁慢,会出现死锁情况
  • 加锁的方式:当执行INSERT,UPDATE,DELETE时InnoDB会自动加排他锁,SELECT不会加任何锁,需要显示加锁:

共享锁:select * from tableName where ... lock in share more
排他锁:select * from tableName where ... for update

3.1.两阶段锁协议

首先我们先了解一下两阶段锁协议,在InnoDB事务中,行锁是在需要的时候在加上,但并不是在不需要的时候则释放,需要等到事务提交之后才释放。这个就是两阶段锁协议

了解这个协议之后,我们使用事务时应该注意,当一个事务需要多个锁时,把最有可能造成冲突,最有可能影响并发度的锁应该尽量往后放

3.2.共享锁

共享锁也称读锁,多用于判断数据是否存在

  1. 多个事务都可以加读锁,可以同时读取同一记录不受影响。
  2. 不允许其他事务加排他锁
  3. 当事务修改数据时,必须要等待先执行的事务commit然后在执行更新操作,不然当并发时很容易造成死锁。
3.3.排他锁

排他锁也称读写锁,独占锁,当前操作没有执行完之前,会阻塞其他的读锁和写锁

当执行INSERT,UPDATE,DELETE时,InnoDB会自动加排他锁

4.死锁和死锁检测

当出现并发时不同的线程出现循环依赖资源,涉及到的线程都在等待其他线程释放资源,从而造成这些线程出现无限等待的状态,这种情况称为死锁。当出现死锁状态时有两种处理策略:

  • 一种是直接进入等待,直到超时,InnoDB可以通过参数innodb_lock_wait_timeout来设置,默认是50s
  • 另一种是发起死锁检测,发现死锁后主动回滚死锁链中的某一条事务,让其他事务可以继续执行。可以使用参数innodb_deadlock_detect=on,表示死锁检测开启

怎么解决热点行更新造成的性能问题
当死锁检测开启时,当并发数达到1000同时去更新同一条数据时,检测死锁这个操作就是100W级别的,造成CPU负载很高,那么每秒执行的事务也很低。怎么去解决这种性能问题了

  • 哪里造成的性能问题就从哪里解决,在确保业务部会出现死锁的情况下可以关闭死锁检测。业务中如果出现死锁时就回滚然后重试这样是无损的,当然死锁也会出现大量的超时,这就对业务有损了
  • 控制并发度,如在客户端控制更新同一行的并发度,当客户端多时服务端的并发度也随之增加。当然可以修改数据库的服务端进行控制,对于更新相同行时排队进入引擎,这样InnoDB内部就不会有那么高的死锁检测了,一般小团队没有能力做到
  • 从表设计上优化,可以通过更新一行的操作改成逻辑上的多行来减少冲突。如在库存表中,一个商品的库存可以放在10条记录中,商品实际库存为几条记录之和,这样每次操作库存发生冲突的概率为原来的1/10,减少了锁的等待个数,也就减少了死锁检测带来的CPU消耗。这种方案就需要控制数据的边界了

参考文献

  • 极客时间《MySQL实战45讲》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值