一.前言
锁主要是用来在多线程或多并发时候限制数据的相关增删改查操作,进而保证数据的一致性和有效性。
MySQL中的锁是在服务器层或者存储引擎层实现的,保证了数据访问的一致性与有效性。
锁的分类:
二.锁粒度区分--全局锁,表锁,行锁
1.全局锁--针对整个数据库的锁。
最常用的全局锁是读锁和写锁。
1.读锁(共享锁):简单说就是阻止其他线程更新数据,但可以读取数据。
2.写锁(排他锁):简单说就是阻止其他线程读取和更新数据。
操作:在MySQL中,可以使用FLUSH TABLES WITH READ LOCK(FTWRL)语句来添加全局读锁,这将阻止其他线程进行更新操作。使用UNLOCK TABLES语句来释放锁定。
缺点:由于锁住了整个数据库的操作,所以全局锁的开销很大,尽量避免在生成环境中使用全局锁。
应用场景:
1.备份全库:使用全局锁可以确保在备份过程中,数据库的所有表都保持一致的状态。例如,可以使用FLUSH TABLES WITH READ LOCK命令在备份期间阻止任何写操作,以确保备份数据的一致性。
2.整体数据迁移:如果你需要将整个数据库从一个服务器迁移到另一个服务器,那么在迁移过程中,你可能希望阻止任何写操作,以确保所有的数据都被正确地迁移到新的服务器。
3.全库只读:在某些情况下,你可能希望将整个数据库设置为只读模式。例如,如果你在进行一些可能破坏数据完整性的操作,或者在进行系统维护时,可能需要将数据库设置为只读模式。在这种情况下,可以使用全局读锁
2.表级锁--对表加锁。
表锁有两种模式:
1表共享读锁(Table Read Lock):简单说就是不允许其他线程更新表,只允许读取表。
2表独占写锁(Table Write Lock):简单说就是不允许其他线程更新和读取表。
使用场景:
1.读密集型和写操作不频繁的应用:如果你的应用主要进行读取操作,很少进行写入操作,那么使用表级锁可能是一个好选择。另外不需要频繁地进行写操作,或者可以容忍写操作的延迟,那么使用表级锁可能是可行的。【读多写少】
2.全表更新或者删除:在某些情况下,可能需要对一张表进行全表的更新或者删除操作,例如,删除表中的所有记录,或者更新表中所有记录的某个字段的值。在这种情况下,使用表级锁是合适的。
3.表中数据量不大
InnoDB发生表级锁的命令:
1.被动加锁
不基于主键或者唯一索引的UPDATE,INSERT,DELETE,TRUNCATE【全表删除】。
2.主动加锁
【1】LOCK TABLES命令 【这个命令可以显式地为一个或多个表加上读锁或写锁】
【2】FLUSH TABLES WITH READ LOCK(FTWRL)命令
【这个命令可以给所有表加上全局读锁,其他会话在此期间不能对数据进行修改】
在MySQL中,MyLSAM只有表锁而没有行锁。
【1】对MyISAM引擎表全表或者大范围扫描的读操作,会自动加上表读锁,对MyISAM表的写操作,会自动加上表写锁。
【2】InnoDB引擎在必要情况下会使用表锁,但主要是使用行锁来实现多版本并发控制(MVCC),它能提供更好的并发性能和更少的锁冲突。简单说对于MyISAM引擎大范围select命令也会加表锁,而InnoDB引擎只有修改数据的命令会加锁,而且会根据情况选择加表锁或者行锁。
3.行锁--对数据库表中的单独一行加锁
InnoDB支持两种类型的行级锁:共享锁(S锁)和排他锁(X锁)。
1.共享锁(S锁):简单说就是不允许其他线程更新该行,只允许读取该行。
2.排他锁(X锁):简单说就是不允许其他线程更新和读取该行。
MySQL那些命令会导致发生行锁?
1.被动加锁
基于主键或者唯一索引的UPDATE,INSERT,DELETE
2.主动加锁
SELECT ... LOCK IN SHARE MODE【select选择一行加上共享锁,即读锁】
SELECT...FOR UPDATE【select选择一行加上写锁】
行锁的问题
1.死锁:当两个或更多的事务相互等待对方释放资源时,就会发生死锁。例如,事务1锁定了行A并试图锁定行B,同时事务2锁定了行B并试图锁定行A,这就形成了死锁。MySQL会检测到死锁并终止其中一个事务,但这仍可能导致性能问题和事务失败。
2.锁升级:如果一个事务试图锁定的行过多,InnoDB可能会将锁从行级升级为表级,这就可能导致更多的锁冲突。
3.性能下降和资源消耗:加锁毫无疑会影响并发的效率,另外加锁本身这个行为也要消耗资源。
4.事务隔离级别:不同的事务隔离级别会影响锁的行为和性能,可能需要根据具体的应用场景来调整事务隔离级别。
三.权限区分--排他锁(写锁)和共享锁(读锁)
排他锁,即写锁,其他线程不能读和写。
共享锁,即读锁,其他线程只读不能写。
附:
如果你的事务隔离级别是READ UNCOMMITTED,那么LOCK IN SHARE MODE可能不会按照你期望的方式工作,因为在这个隔离级别下,事务可以读取其他事务未提交的修改。在READ COMMITTED,REPEATABLE READ,和SERIALIZABLE隔离级别下,LOCK IN SHARE MODE能正常工作。
四.应用上的区分--乐观锁和悲观锁
乐观锁:乐观,认为线程安全问题不一定会发生。只在更新数据时去判断有没有其他线程对数据做了修改。
悲观锁:认为线程安全问题一定会发生。因此在对更新数据的一系列操作之前先获取锁,确保线程串行执行。
总结而言一个比较乐观,在更新数据时候才加锁,一个比较悲观,在更新数据前就加锁。
1.乐观锁
在MySQL中,乐观锁并没有内置的实现,但是可以通过一些编程技巧来实现。一种常见的实现方式是使用版本号(或时间戳)字段。每当一条记录被修改时,就增加版本号(或更新时间戳)。在更新记录时,先检查版本号(或时间戳)是否和读取记录时的版本号(或时间戳)一致,如果一致则执行更新并增加版本号(或更新时间戳),否则就拒绝更新。这样就可以保证只有当记录没有被其他事务修改时,当前事务的更改才会被提交。
乐观锁的优点在于,由于大部分时间都不需要锁定,所以在冲突较少的情况下可以获得较高的并发性能。
缺点:1.锁本身的开销和性能
2.如果冲突较多,那么乐观锁可能会导致大量的事务回滚,从而影响性能。因此,选择使用乐观锁还是其他锁定技术,需要根据实际的并发情况和性能需求来决定。
3.增加代码复杂性
乐观锁适合于一下的应用场景:
2.悲观锁
实现(在事务开启前加锁):
【1】加上表锁
LOCK TABLES Orders WRITE;//加上写锁
-- 在这里执行你需要的操作
UNLOCK TABLES;
【2】加上行锁排他锁(写锁)
START TRANSACTION;
SELECT * FROM Orders WHERE OrderID = 1 FOR UPDATE;
-- 修改你需要的数据然后提交事务
UPDATE Orders SET Quantity = 10 WHERE OrderID = 1;
COMMIT;
【3】使用行锁共享锁(读锁)
START TRANSACTION;
SELECT * FROM Orders WHERE OrderID = 1 LOCK IN SHARE MODE;
-- 你可以在这里安全地读取数据
COMMIT;
五.加锁取锁的底层优化--意向锁
意向锁是 MySQL 自带的一种锁机制。
意向锁是一种在数据库中用于控制并发访问的机制。当一个事务需要访问某个资源(如数据行、表)时,会先尝试获取意向锁,然后再获取实际的锁。
这样做可以减少死锁的概率,并提高并发访问效率。
简单说意向锁就是在整个事务开始的时候先声明要锁定哪些资源。
当两个或更多的事务相互等待对方释放资源时,就会发生死锁。例如,事务1锁定了行A并试图锁定行B,同时事务2锁定了行B并试图锁定行A,这就形成了死锁。MySQL会检测到死锁并终止其中一个事务,但这仍可能导致性能问题和事务失败。而如果在事务1开始时先加上意向锁声明要锁定行A和行B,则可以避免死锁的发生。而要获取锁的时候不是直接去请求锁,而是去请求意向锁。
另外,意向锁是表级锁,只需要判断一次即可知道有没数据行被锁定,而不用遍历判断每一行数据,提升性能。
六.其他特殊的锁--间隙锁和临键锁
1.间隙锁--所定两个索引之间的区间
间隙锁(Gap Locks)是MySQL InnoDB存储引擎提供的一种锁定机制。它锁定的不是具体的行记录,而是两个索引之间的间隙(或者说区间),这样可以防止新的记录插入到该间隙,确保数据的一致性和事务的隔离性。
间隙锁常常与记录锁(Record Locks)一起使用,共同形成Next-Key锁,保护索引记录的范围查询和扫描操作。
以下是间隙锁的主要类型:
1 区间-区间间隙锁:锁定两个索引键之间的间隙,或者是第一个索引键之前的间隙。
2 区间-记录间隙锁:锁定一个索引键和一个记录之间的间隙。
3 记录-区间间隙锁:锁定一个记录和一个索引键之间的间隙。
间隙锁的存在,主要是为了解决幻读问题。所谓幻读,是指在一个事务内读取某个范围的记录时,另外一个事务在该范围内插入了新的记录,当第一个事务再次读取该范围的记录时,会发现有些原本不存在的记录,这就是幻读。
举例来说,假设我们有一个存储学生信息的表,有一个事务A要查询年龄在10-20之间的学生,它在查询前会对这个区间加锁。此时如果有另一个事务B想要插入一个年龄为15的学生,由于这个年龄的范围已经被事务A锁定,所以事务B必须等待,直到事务A完成,释放锁。这样就避免了幻读的产生。
值得注意的是,由于间隙锁会锁定范围,如果并发事务较多且涉及的数据范围有交集,可能会引发性能问题,甚至死锁。因此,在设计数据库和选择隔离级别时,需要综合考虑数据一致性和并发性能。
2.临键锁
Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
总结:根据非唯一索引列上的值锁住一段左开右闭区间的数据。
id | age | name |
1 | 10 | 张三 |
3 | 24 | 李四 |
5 | 32 | 王五 |
7 | 45 | 赵六 |
该表中 age 列潜在的临键锁有: |
(-∞, 10], |
(10, 24], |
(24, 32], |
(32, 45], |
(45, +∞], |
3.记录锁
就是我们普通的锁,只是锁定一行。
总结:间隙锁和临键锁可以通过相应的算法锁定多行,而记录锁是能够锁定一行。