
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
普通的 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;
加锁的对象是索引,加锁的基本单位是 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 锁会退化成间隙锁
这里避免了同age而主键id靠后,逃过Next-key锁后的插入,避免幻读现象,
由于间隙锁是左开右开的,对于边界上的位置能否插入新纪录,根据**「二级索引值(age列)+主键值(id列)」**同时判断插入位置后面是否具有Gap锁。
所以也可以看到
LOCK_DATA:39,20
,增加一个字段来判断哪些范围的id值可以插入。
非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。
因为在二级索引中字段不是唯一的,如果只加记录锁,无法防止插入或者修改,出现幻读。
如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。
在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。
表级锁
修改表结构会锁表,因此在修改表结构时,影响表的写入操作;
数据越多,锁表时间越长。
修改失败,还原表结构,耗时长
如果修改表结果失败,必须还原表结构,所以耗时更长;
比如:添加一个唯一性约束,结果发现很多数据有控制,无法添加进来了,这个时候就只能还原表结构
大数据表记录多,修改表结构锁表时间很久、
由于 alter table 线上修改表结构有诸多弊端,但是 PerconaTookit 提供了一个开源的线上修改表结构的工具。
其中一个名为 pt-online-schema-change 的工具可以完成在线修改表结构。
普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以显式使用:
begin;
//对读取的记录加共享锁
select ... lock in share mode;
commit; //锁释放
begin;
//对读取的记录加排他锁
select ... for update;
commit; //锁释放
行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。
如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行记录加上了行锁,还给行记录两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。
在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞。
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。
在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
设置事务等待锁的超时时间。
innodb_lock_wait_timeout
,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。开启主动死锁检测。
当一个事务的等待时间超过该值或者主动死锁检测到死锁后,主动回滚到链条中某一个十五,让其他事务正常运行,
一般情况是由于批量更新时加锁顺序不一致而导致的死锁。
悲观锁(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 Set,比较并设置。
1、比较:读取到了一个值 A,在将其更新为 B 之前,检查原值是否仍为 A(未被其他线程改动)。
2、设置:如果是,将 A 更新为 B,结束。[1]如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在 CPU 看来就是一步操作。有了 CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS 利用 CPU 指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
解决办法:
我们需要加上一个版本号(Version),在每次提交的时候将版本号+1 操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息。