锁的概述
文章目录
前面我们已经学习过redolog和undolog, 通过这两种日志完成了事物的原子性, 持久性, 一致性的保证, 那么隔离性是如何保证的, 其实就是我们这里要学习的锁来解决的
概述:
锁是计算机协调多个进程或者线程并发访问某一资源的机制. 在程序开发中会存在多线程同步的问题, 当多个线程并发访问数据的时候, 尤其是针对一些敏感的数据(比如 : 订单, 金额等), 我们就需要保证这个数据在任何时刻最多只有一个线程在访问, 保证数据的完整性和一致性, 在开发过程中加锁是为了保证数据的一致性, 这个思想在数据库领域中同样很重要.
在数据库中, 除了传统的计算资源(如: cpu, RAM, I/O等)的争用以外, 数据也是一种供许多用户共享的资源. 为了保证数据一致性, 需要对并发操作进行控制, 因此产生了锁. 同时锁机制也为实现MySQL的各个隔离级别提供了保证. 锁冲突也是影响数据库并发访问性能的一个重要因素. 所以锁对数据库而言显得尤其重要, 也更加复杂.
MySQL并发事物访问相同记录:
- 注意: 这里说的相同记录应该说是同一级别的记录会更加合适, 比如如果是行锁的时候那么就是同一条记录, 如果是表锁的时候同一个表中的记录就可以称之为此时的相同记录
并发事务访问相同记录的情况大致可以划分为三种:
1.读-读情况:
读-读情况, 即并发事务相继读取相同的记录, 读取操作本身不会对记录有任何影响, 并不会引起什么问题, 所以允许这种情况的发生
2.写-写情况:
写-写情况, 即并发事物相继对相同的记录做出改动
这种情况下回发生脏写(脏写, 比如一个事务中将1修改为了2, 并且进行提交, 另一个事务将1修改为了3, 并且进行了回滚事物, 最终数据为1, 那么前一个事务以为自己修改为了2就是一种脏写)的问题, 任何一种隔离级别都不允许这种问题的发生. 所以在多个未提交事务相继对一条记录做改动时, 需要让它们排队执行, 这个排队的过程其实是通过锁实现的. 这个所谓的锁其实是一个内存中的结构, 在事物执行前本来是没有锁的, 也就是锁一开始是没有锁结构和记录进行关联的, 如图所示
当一个事务想对这条记录做改动的时候, 首先会看看内部才能中有没有与这条记录关联的锁结构, 当没有的时候就会在内存中生成一个锁结构与之关联. 比如, 事务T1要对这条记录做改动, 就需要生成一个锁结构与之关联
在锁结构里有很多信息, 为了简化理解, 只把两个比较重要的属性拿了出来:
当事务T1改动了这条记录之后, 就生成了一个锁结构与该记录关联, 因为之前没有别的事务为这条记录加锁, 所以is_waiting属性就是false, 我们把这个场景就称之为获取锁成功, 或者加锁成功, 然后就可以继续执行操作了
在事务T1提交之前, 另一个事务T2也想对该记录做改动, 那么先看看有没有锁结构与这条记录关联, 发现有一个锁结构与之关联后, 然后也生成了一个锁结构与这条记录关联, 不过锁结构的is_waiting属性值为true, 表示当前事务需要等待, 我们把这个场景就称之为获取锁失败, 或者加锁失败, 图示:
在事务T1提交之后, 就会把该事务生成的锁结构释放掉, 然后看看还有没有别的事务在等待获取锁, 发现了事务T2还在等待获取锁, 所以把事务T2对应的锁结构的is_waiting属性设置为false, 然后把该事务对应的线程唤醒让它继续执行, 此时事务T2就获取到锁了. 效果图如下:
小结几种说法:
-
不加锁
意思就是不需要在内存中生成对应的锁结构, 可以直接执行操作
-
获取锁成功, 或者加锁成功
意思就是在内存中生成了对应的锁结构, 而且锁结构的is_waiting属性为false, 也就是事物可以继续执行操作
-
获取锁事变, 或者加锁失败, 或者没有获取到锁
意思就是在内存中生成了对应的锁结构, 不过锁结构的is_waiting属性为true, 也就是事物需要等待, 不可以继续执行操作
3. 读-写 或 写-读情况
读-写 或 写-读, 即一个事物进行读取操作, 另一个事物进行修改操作, 这种情况下可能发生脏读, 不可重复读, 幻读的问题.
各个数据库厂商对SQL标准的支持都可能不一样, 比如MySQL在repeatable read隔离级别上就已经解决了幻读的问题
并发问题的解决方案:
怎么解决脏读, 不可重复读, 幻读这些问题呐? 其实有两种可选的解决方案:
方案一 : 读操作利用多版本并发控制(MVCC), 写操作进行加锁
所谓的MVCC, 就是生成一个ReadView, 通过ReadView找到符合条件的记录版本(历史版本由undo日志构建). 查询语句只能读到在生成ReadView之前已提交事物所做的更改, 在生成ReadView之前未提交的事物或者之后才开启的时候所做的更改是看不到的. 而写操作肯定针对的是最新版本的记录, 而写操作肯定针对的是最新版本的记录, 读记录的历史版本和改动记录和最新版本本身并不冲突, 也就是采用MVCC时, 读-写操作并不会冲突
普通的select语句在read committed和repeatable read隔离级别下会使用到MVCC读取记录
在read committed隔离级别下, 一个事物在执行过程中每次执行select操作时都会生成一个ReadView, ReadView的存在本身就保证了事物不可以读取到未提交的事物所做的更改, 也就是避免了脏读现象
-
在repeatable read隔离几倍下, 一个事物在执行过程中已有第一次执行select操作才会生成一个ReadView, 之后的select操作都是复用这个ReadView, 这样也就避免了不可重复读和幻读的问题
-
- 按照官方文档对幻读的定义 : 是同一个事物中两次查询的结果的行记录不同, 那么repeatable read的隔离级别下, 因为在一个事物执行过程中只有第一次select的时候会生成一个ReadView, 所以确实是解决了不可重复读和幻读
- 但是还有一种对幻读的说法 : 是查询的时候查询不出这条记录, 但是尝试在该记录位置插入数据的时候发现数据已经存在了, 这种说法的幻读在repeatable read隔离级别下是还是存在的
方案二 : 读,写操作都采用加锁的方式
如果我们的一些业务场景不允许读取记录的旧版本, 而是每次都必须读取记录的最新版本. 比如, 在银行存款的事物中, 你需要先把账户的余额读出来, 然后将其加上本次存款的数额, 最后再写到数据库中. 在将账户余额读取出来后, 就不想让别的事务再访问该余额, 直到本次存款事物执行完成, 其他事物才可以访问账户的余额.这样在读取记录的时候就需要对其进行加锁操作, 这样也就意味着读操作也可以像写-写操作那样排队执行 --> 后面我们就会说: 其实读锁也是有互斥锁的
脏读的产生是因为当前事物读取了另一个未提交事物写的一条记录, 如果另一个事物在写记录的时候就给这条记录加锁, 那么当前事物就无法继续读取该记录了, 所以也就不会有脏读问题的产生了 --> 通过写互斥锁
不可重复读的产生是因为当前事物先读取一条记录, 另外一个事物对该记录做了改动并提交之后, 当前事物再次读取时会获得不同的值, 如果在当前事物读取记录时就给该记录加锁, 那么另一个事物就无法修改该记录, 自然也不会发生不可重复读了 --> 通过读互斥锁
幻读问题的产生是因为当前事物读取了一个范围的记录, 然后另外的事物向该范围内插入了新记录, 当前事物再次读取该范围的记录时发现了新插入的新纪录. 采用加锁的方式解决幻读问题就有一些麻烦, 因为当前事物在第一次读取记录时幻影记录并不存在, 所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁) --> 通过间隙锁
小结对比:
- 采用MVCC方式的话, 读写操作彼此不冲突, 性能更高
- 采用加锁方式的话, 读写操作彼此需要排队执行, 影响性能
一般情况下我们当然愿意使用MVCC来解决读-写操作并发执行的问题, 但是业务在某些特殊情况下, 要求必须采用加锁的方式执行, 下面就讲解下MySQL中不同类别的锁