摘要
摘要:乐观锁;悲观锁;实现方法;本地锁;分布式锁;死锁;行级锁;表级锁
1 锁的相关概念
1.1 为什么需要锁?
问题:
- ① 在多个线程访问共享资源时,会发生线程安全问题,例如:在根据订单号生成订单时,若用户第一次由于某种原因(网络连接不稳定)请求失败,则会再次发生请求,此时便会产生同一订单号生成多个订单,这显然是有问题的。
解决:
- ① 针对上述问题,我们有一个解决思想,给用户第一次的请求加锁,只有当前第一次请求拥有锁,请求线程在拥有锁时,方可执行,其他线程必须在拥有锁的线程执行完毕后,方可执行。
1.2 本地锁
问题:
- ① 目前的系统架构,大体分为两类:一类是单体架构,另一类是分布式架构(在分布式架构中为保障系统的高可用,我们又会搭集群),
- ② 针对上述两类架构的特点,锁又分成两种不同的类别:一类是针对单体架构的锁,称之为本地锁,另一类是针对分布式架构的锁,称之为分布式锁。
- ③ 针对本地锁,又有两类:一类是在高并发场景下,编程语言实现对自己多线程控制的本地锁,诸如:Java语言中synchronized、Lock本地锁(同时也是悲观锁),另一类是在数据库中实现的锁思想,诸如:乐观锁、悲观锁、共享锁、排它锁、记录锁、间隙锁、表锁等本地锁,其都可以称之为本地锁,保证业务数据的准确性。
解决:
- ① 本地锁:针对单体架构项目高并发特点,有两类解决方案:一类是语言自己实现的,例如Java语言的synchronized、Lock锁,另一类是在数据库中实现的锁思想,例如乐观锁与悲观锁,本文也着重于此两点说明。
- ② 分布式锁:由于编程语言自己实现的锁,无法满足在分布式架构中多链路调用情况,因而出现分布式锁的思想他的解决主要有:Redisson、zookeeper、数据库(数据库性能低,使用场景少),详情请参阅另一篇文章:[分布式锁]:Redis与Redisson
2 乐观锁与悲观
通俗理解:乐观锁,对一件事持乐观态度,认为大概率不会发生;悲观锁,对一件事持悲观态度,认为大概率会发生。
2.1 乐观锁
2.1.1 乐观锁的概念
概念:认为大概率不会发生线程安全问题。
2.1.2 乐观锁的解决思想
解决思想:通过在数据上添加标识(如版本号或时间戳)来进行并发控制,实现线程安全的共享数据访问。
2.1.2.1 数据版本号机制思想
① 首先,给数据库添加一个字段version(int)的标记字段,
② 随后,当多个线程同时访问数据库时,都会获得version的值,
③ 然后,在提交更新时若刚才读取到的version为当前数据库中中version值时才更新,伴随着更新过后version的值也会发生变化,
④ 最后,当其他线程需要提交更新时,获取到的version值和当前数据库version值不一样,提交更新失败,从而实现对线程安全的控制。
2.1.2.1.1 数据版本号机制实现——基于mybatis
引入业务场景:假设数据库中账户有一version字段(值为1),且当前账户余额balance字段(值为100)
- ① 操作员A此时将其读出(此时version=1),并从账户余额中扣除50(100-50),
- ② 操作员A操作的同时,操作员B也读出此账户信息(此时version=1),并从账户余额扣除20(100-20),
- ③ 操作员A先完成了修改工作,并且,将数据版本号(version=1)和账户扣除后余额(balance=50),提交至数据库更新,此时由于提交数据版本=当前数据库记录版本,数据被成功更新,数据版本更新为2(version=2),
- ④ 操作员B完成操作后,也将读出到的数据版本(version=1)和账户扣除后余额(balance=80),提交至数据库请求更新,此时数据库数据版本已经被更新(version=2),不满足数据版本号相同时,才能更新数据的策略,因此操作员B请求被驳回。从而保证数据的的准确。
2.1.2.1.1.1 实体类中添加响应字段,并设定当前字段用于记录数据的版本信息
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
2.1.2.1.1.2 使用乐观锁前必须先获取对应数据版本号
注意:由于在使用乐观锁时需要跟数据库频繁进行交互,因而在高并发场景下,建议使用分布式锁和悲观锁(是的,乐观锁也可以作为分布式锁来使用)。
@Test
public void testUpdate() {
/*User user = new User();
user.setId(3L);
user.setName("Jock666");
user.setVersion(1);
userDao.updateById(user);*/
//1.先通过要修改的数据id将当前数据查询出来
//User user = userDao.selectById(3L);
//2.将要修改的属性逐一设置进去
//user.setName("Jock888");
//userDao.updateById(user);
//1.先通过要修改的数据id将当前数据查询出来
User user = userDao.selectById(3L); //version=3
User user2 = userDao.selectById(3L); //version=3
user2.setName("Jock aaa");
userDao.updateById(user2); //version=>4
user.setName("Jock bbb");
userDao.updateById(user); //verion=3?条件还成立吗?
}
2.1.2.2 CAS算法思想
- 实现思想:CAS算法基于ccompare-and-swap(比较和交互)操作,类似于Junit的断言机制,其通过比较当前值和期望值的方式,实现乐观锁的并发控制机制。
- 使用说明:在使用时,先读取数据的原值,根据规则计算出新的期望值,随后,使用CAS操作把期望值写入数据的存储位置,若操作成功,说明没有发生冲突,更新操作可以提交,否则,操作失败,需要重新读取数据并重复以上操作。
- 问题:一方面是数据库性能问题,另一当面是ABA问题。
- ABA问题:当前有ABC三个线程,初始数据版本号为V1,线程AB分别查询到该数据的版本号是V1,线程A先更新数据,把版本改为A2,然后线程C也执行一次操作把版本号从V1更改成V3随后又改回V1,此时当线程B更新时发现数据版本匹配,更新操作成功,但实际数据已经被C修改,因此为避免此问题又需借助时间戳、版本号机制来解决。
2.2 悲观锁
2.2.1 悲观锁的概念
悲观锁:认为大概率会发生线程安全问题。
2.2.2 悲观锁的解决思想
悲观锁的核心思想:在操作共享数据之前对其进行加锁,保证同一时刻只有一个线程可以访问,避免数据的并发修改和读取。
2.2.3 悲观锁的实现方式
2.2.3.1 基于数据库机制
基于数据库机制,诸如:行级锁和表级锁等,通过在数据库上对共享数据进行加锁,保证数据的一致性和完整性。
2.2.3.1.1 行级锁
行级锁:是在对数据库表进行操作第,对操作进行的行加锁,即SQL语句在修改每行记录时,只会锁定该行数据,不会影响其他行,其他记录仍可被修改,行级锁可以有效提高并发性,然而由于加锁粒度小,因此在
短事务
(诸如简单的update、delete、insert操作)并发场景下会影响性能。
- 如:在MySQL的InnoDB中提供的就是行级锁。
- 注:长事务(占用锁的粒度比较大,时间长,通常会涉及多个表,多次修改,对数据库影响性能较大)
2.2.3.1.2 表级锁
表级锁:MySQL中MyISAM存储引擎使用的是表级锁,当一个事务对一个表进行操作时,MyISAM会给整个表加锁,其它事务无法对该表进行读取或修改操作,直至锁释放,然而由于加锁粒度大,因此会带来性能上的损失,不适用于并发更新操作比较多的场景。
2.2.3.2 基于应用层面的锁机制
基于应用层面的锁机制,如synchronized锁和lock锁等,通过在代码层面对共享数据进行加锁,实现数据的并发控制。
2.2.3 悲观锁缺点
2.2.3.1 性能瓶颈
性能瓶颈:需要早操作共享数据前加锁,阻塞其它线程对数据的访问,造成性能瓶颈。
2.2.3.1 死锁问题
死锁题锁:在锁定期间可能会出现死锁问题,即在加锁操作过程中出现异常或造成线程长时间未释放,就可能发生死锁问题,导致应用程序崩溃。
2.3 死锁
2.3.1 死锁的概念
死锁:两个或多个进程,因相互申请对方占用的资源而造成互相等待的现象,导致所有进程都在等待彼此释放资源而无法继续向前推荐,最终都无法完成任务。
2.3.2 死锁出现原因
死锁出现原因:由于多个线程或进程在运行过程中,因相互申请对方占用资源而造成互相等待的现象,从而,出现一种无法解决的状态。文章来源:https://www.toymoban.com/news/detail-478529.html
案例:文章来源地址https://www.toymoban.com/news/detail-478529.html
- 两个线程AB同时占用一些资源,现在A需要获取B占用资源,而B同时需要获取A占用资源,两个线程都不释放已占用资源,从而造成死锁状态。
2.3.3 死锁解决方法
- ① 预防死锁:通过合理资源申请和释放策略避免死锁发生。
- ② 检测死锁:采用算法检测死锁状态,并及时采取相应措施,诸如撤销一些进程、杀掉进程等。
- ③ 解除死锁:采用一定策略释放资源、终止进程等以打破死锁状态。
- ④ 避免死锁:从资源分配角度出发,在分配每一个资源时都要判断是否会出现死锁,若会出现,则不分配资源。
到了这里,关于[锁]:乐观锁与悲观锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!