栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

[MySQL] InnoDB 锁详解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

[MySQL] InnoDB 锁详解

除非有什么特别的原因必须使用其他储存引擎, 不然 MySQL 就应该优先考虑 InnoDB

锁的分类

按照锁的细粒度分为

行级锁页级锁(分段锁)表级锁 乐观锁和悲观锁

锁的分类按照使用方式分为乐观锁和悲观锁, 乐观锁和悲观锁是 2 钟思想, 跟编程语言和数据库没有关系

乐观锁: 乐观锁不会上锁, 只是在数据更新的时候数据是否被并发改变, 如果改变了就放弃操作悲观锁: 悲观锁就是一般的锁, 会上锁, 上锁期间其他人不能修改数据

乐观锁主要有 2 种实现方式, CAS 和 版本号

1. CAS 实现乐观锁

CAS 是 Compare And Swap, 原子性的执行操作: 检查内存中的值是否符合预期, 符合就执行修改(swap), 不符合就操作失败(一般的乐观锁的实现都会遍历多次 CAS 直到操作成功)

golang atomic 包下的 CAS 函数

// CompareAndSwapInt32 executes the compare-and-swap operation for an int32 value.
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

golang Mutex 互斥锁就是用 CAS 获取锁的

func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false
	iter := 0
	old := m.state
	//慢速模式进来就是一个大的 for 循环
	for {
        ...
		// 3. 尝试 atomic 的 compareAndSwap 函数更新状态, 锁是通过 atomic 获取的.
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			//(1) 如果通过原子 atomic 操作获取锁成功,就结束循环
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS 获取锁就退出
			}
			// If we were already waiting before, queue at the front of the queue.
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			//(2) atomic 获取锁失败就在队列中等待, runtime 的 runtime_SemacquireMutex() 等待的方法可以通过信号量通信.
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			//(3) 还会通过 staving 时间更新互斥锁的 staving 状态,如果当前队列只有该线程, starving 状态退出
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			//(4) 处理些边角情况
            ...
}
2. CAS 如何保证原子性?

CAS 是 CPU 支持的原子性操作, 硬件层面保证原子性.

3. CAS 有什么缺点

ABA 问题:

内存中的值被并发修改但是最后和预期一样, CAS 就会成功, 但是这并不是原子性

一般的情况没啥问题, 因为反正值就是在预期的, 比如 golang 的 Mutex 就是这样实现的

有些情况比如某些操作只能做一次就不行, 举个例子就是栈顶问题

解决方法就是进入版本号双重控制

高竞争开销:

CAS 失败后一般会不断尝试直到成功(比如加锁), 所以在竞争很高的情况下,比如 100 个并发竞争修改一个, 99 个都在不断尝试获取锁又不休眠, 开销可想而知

解决方法就是用悲观锁(高竞争就是用悲观锁, 低竞争采用乐观锁)

功能限制:

CAS 只支持单个变量的 CAS

4. 版本号实现乐观锁

思路就是在数据中增加要给字段 version , 每次更新前面对比下版本号是否一致, 更新后版本号 +1

只有版本一致才修改成功, 版本不一致就放弃修改

很多数据库表都有这种设计, spring 的 mybatis-plus ORM 框架能直接通过 @version 注解到字段上实现这个功能

5. 什么是否使用乐观锁

竞争小的时候使用乐观锁, 竞争大的时候使用悲观锁

InnoDB 中加锁 1. InnoDB 锁类型

共享锁/读锁: 行锁排他锁/写锁: 行锁意向共享锁: 表锁, 内部调用意向排他锁: 表锁, 内部调用 2. 使用方式

开发者能使用的锁只有共享锁和排他锁, 因为意向锁值 InnoDB 内部调用的表锁, 不支持开发者使用

共享锁: 读锁, 只读, 允许并发读排他锁: 写锁, 锁定之后只能该实例操作, 不能并发读写

InnoDB 我们在 update 和 delete 的时候默认就是 排他锁, 一般加锁都是在 select 的时候

# 共享锁
select name from user where id = 1 lock in share mode;
# 排他锁
select name from user where id = 1 for update;

其实我们开发更加常用加乐观锁方式, 就是表中增加的 version 字段做版本控制的乐观锁, 很多框架都有乐观锁的支持,比如 mybatis-plus 通过 @version 注解就能从框架层实现乐观锁

3. 分析数据库的锁的性能

记录慢SQL来分析锁的性能, 我们也能直接查询到 MySQL 自己统计的 metric 数据

mysql>  show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 2     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 6     |
+-------------------------------+-------+
5 rows in set (0.04 sec)

mysql> 
4. InnoDB 什么时候表锁

InnoDB 的行锁是基于索引来实现的如果条件没有用到索引或者索引失效将导致表锁(应该尽力避免!!)

比如下面 name 没有在索引里将会导致表锁

update user set name = '张三' where name = '李四';
refrence

MySQL InnoDB引擎锁的总结 - SegmentFault 思否

【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗? - 编程迷思 - 博客园 (cnblogs.com)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/732491.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号