数据库锁机制

  • MySQL锁的结构图
  • 一. 悲观锁
  • 二. 乐观锁
  • 三. InnoDB引擎的锁机制(属于悲观锁)
  • ①.共享锁/读锁
  • ②.排他锁/写锁
  • ③.意向共享锁
  • ④.意向排他锁
  • ⑤.行锁
  • ⑥.表锁
  • ⑦.页锁(又叫Gap锁/间隙锁)
  • 四. MVCC(Multi-Version Concurrency Control,属于乐观锁)
  • 五. Mysql(Innodb引擎)的一些默认机制
  • 六.数据库的事务隔离级别操作


MySQL锁的结构图

mysql意图锁 mysql锁实现原理_数据库

一. 悲观锁

悲观锁

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

二. 乐观锁

乐观锁:(不能解决脏读的问题)

  • 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制或CAS算法实现。
  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量,java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

数据版本(Version)/时间戳 (Timestamp)

  • 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。
  • 为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的version字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。
  • 当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  • 或者在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

CAS算法

  • CAS算法即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
1. 需要读写的内存值 V
	2. 进行比较的值 A
	3. 拟写入的新值 B
  • 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
  • 乐观锁适用于写比较少的情况下(并发量大/多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

三. InnoDB引擎的锁机制(属于悲观锁)

  • 以InnoDB为主介绍锁,是因为InnoDB支持事务,支持行锁和表锁用的比较多,Myisam不支持事务,只支持表锁。

按照锁的使用方式可分为:共享锁、排它锁、意向共享锁、意向排他锁

①.共享锁/读锁

  1. 对于共享锁来说,一个事务获取了某一行的共享锁,则其他事务还可以继续获取这一行的共享锁,但是不能获取这行的排它锁。
  2. 在select语句后面加 lock in share mode,即可获取共享锁。
  3. 在有事务获取了共享锁之后,其他事务是不能做insert/update/delete,因为insert/update/delete语句,是自动加上排它锁的
  4. 如何判断获取的是行锁还是表锁,SELECT * from city where id = “1” lock in share mode; 这个sql里,如果id这个字段带了索引,则获取的是行锁,否则获取的是表锁

②.排他锁/写锁

  1. 一个事务获取了某一行的排他锁,其他事务不能再获取任何锁(包括共享锁和排它锁)
  2. 在select语句后面加 for update,即可获取排它锁。
  3. insert/update/delete语句,是自动加上排它锁的
  4. 如何判断获取的是行锁还是表锁,SELECT * from city where id = “1” for update; 这个sql里,如果id这个字段带了索引,则获取的是行锁,否则获取的是表锁

③.意向共享锁

  1. 通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时innodb会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。
  2. 检测表锁和行锁的冲突

④.意向排他锁

  1. 通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加排他锁,那么此时innodb会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。
  2. 检测表锁和行锁的冲突

按照锁的粒度可分为:行锁、页锁(间隙锁)、表锁

⑤.行锁

  1. 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件来检索数据才会用到行锁,否则InnoDB将会使用表锁。
  2. 行锁:select * from table_name where id = 1 for update 。id 字段为唯一索引字段,所以使用的就是行锁,且是排它锁。
  3. 行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。
  4. 由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁
  5. 使用行级锁定的主要是InnoDB存储引擎, InnoDB有三种行锁的算法
  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  • Next-Key Lock:行锁+间隙锁,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

⑥.表锁

  1. LOCK TABLE name READ; 用读锁锁表,会阻塞其他事务修改表数据。
  2. LOCK TABLE name WRITE; 用写锁锁表,会阻塞其他事务读和写。
  3. 表锁:select * from table_nane where name = ‘小巷’ for update 。name字段不是唯一索引字段,所以是表锁。(表锁 排他锁)
  4. 表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题

⑦.页锁(又叫Gap锁/间隙锁)

  1. 页锁(又叫Gap锁/间隙锁):所谓表锁锁表,行锁锁行,那么页锁折中,锁相邻的一组数据。
  • 通过加锁控制,可以保证数据的一致性,但是同样一条数据,不论用什么样的锁,只可以并发读,并不可以读写并发(因为写的时候加的是排他锁所以不可以读),这时就要引入数据多版本控制来实现读写并发

四. MVCC(Multi-Version Concurrency Control,属于乐观锁)

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照

数据多版本实现的原理是

1,写任务发生时,首先复制一份旧数据,以版本号区分
	
	2,写任务操作新克隆的数据,直至提交
	
	3,并发读的任务可以继续从旧数据(快照)读取数据,不至于堵塞

MVCC实现

  • Innodb的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。一个列保存了行的创建时间,一个保存行的过期时间(或删除时间)(这边说的时间不是具体的时间值,而是系统版本号,每开启一个新事务,版本号递增)。
  • MVCC只在REPEATABLE Read(可重复读)Read uncommited(读不可提交) 隔离级别下工作

MVCC可以为数据库解决以下问题

  1. 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  2. 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

快照读和当前读

  1. 快照读 (snapshot read):读取的是快照版本,也就是历史版本,快照读的实现是基于多版本并发控制,即MVCC,不用加锁
# 简单select操作,属于快照读(不加锁)
	select * from table where ? ;
  1. 当前读:读取的是最新版本,,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
//特殊的读操作,插入/更新/删除操作,属于当前读
	select * from table where ? lock in share mode; //查询加共享锁(S锁)
	select * from table where ? for update;  //查询加排他锁(X锁)
	
	//都是加排他锁(X锁)
	insert into table values (...); //插入
	update table set ? where ?; //更新
	delete from table where ?;  //删除
  1. 不加锁的select操作就是快照读,串行级别下的快照读会退化成当前读。
  2. update、delete、insert(排他锁)、select… Lock in share mod(共享锁)、select… for update(排他锁)是当前读。
  3. 排它锁(for update) 是 串行执行
  4. 共享锁(Lock in share mod) 是 读读并发
  5. 数据多版本并发控制是 读写并发

乐观锁和悲观锁比较

  1. 乐观锁利用MVCC实现一致性非锁定读,这就有保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题,也可以解决幻读问题;
  2. 悲观锁,Serializable隔离级别,利用Gap Locks(间隙锁)、表锁可以阻止其它事务在锁定区间内插入数据,可以解决幻读问题。

五. Mysql(Innodb引擎)的一些默认机制

Mysql(Innodb引擎)的一些默认机制

  1. 数据库并发问题,主要通过设置事务隔离级别来解决,而事务隔离级别一般则通过锁机制的实现;
  2. MySQL默认隔离级别(RR可重复读)使用MVCC+锁混合的模式来解决脏读、不可重读、幻读等问题。
  3. 默认的事务级别为:可重复读级别(RR);(可通过设置进行更改)
  4. 默认锁级别为:行锁;(可通过设置进行更改)
  5. Where筛选条件中使用索引字段的,加的是行锁;不是使用索引字段筛选的,加的是表锁。
  6. 意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理;
  7. 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;
  8. 对于普通SELECT语句,InnoDB不会加任何锁;(可以自己手动上锁)

六.数据库的事务隔离级别操作

查看数据库全局事务隔离级别

SELECT @@GLOBAL.tx_isolation;

查看数据库当前会话事务隔离级别

SELECT @@SESSION.tx_isolation;
或者
SELECT @@tx_isolation;

设置全局事务隔离级别

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

设置当前会话事务隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
或者
SET tx_isolation = 'read-committed';