[锁]:乐观锁与悲观锁

文章目录

  • 摘要
  • 1 锁的相关概念
    • 1.1 为什么需要锁?
    • 1.2 本地锁
  • 2 乐观锁与悲观
    • 2.1 乐观锁
      • 2.1.1 乐观锁的概念
      • 2.1.2 乐观锁的解决思想
        • 2.1.2.1 数据版本号机制思想
          • 2.1.2.1.1 数据版本号机制实现——基于mybatis
            • 2.1.2.1.1.1 实体类中添加响应字段,并设定当前字段用于记录数据的版本信息
            • 2.1.2.1.1.2 使用乐观锁前必须先获取对应数据版本号
        • 2.1.2.2 CAS算法思想
    • 2.2 悲观锁
      • 2.2.1 悲观锁的概念
      • 2.2.2 悲观锁的解决思想
        • 2.2.2.1

摘要

摘要:乐观锁;悲观锁;实现方法;本地锁;分布式锁

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.2.1

你可能感兴趣的:(数据库,java,开发语言)