• 《大厂高并发分布式锁从入门到实战》之第1讲分布式锁背景介绍及jvm锁和数据库锁


           

    目录

           

    前言

    前置要求

    一、JVM本地锁失效的三种情况

    二、数据库悲观锁

    三、mysql 锁总结



            前言

            随着信息化技术的发展,近年来越来越多的电商平台呈现在人们的视线中,一个好的电商平台自然能够对外稳定持续的运行,为大众提供便捷的足不出户线上购买商品服务,但是当用户量达到一定的数量之后,经典的库存超卖问题是必须要考虑的。

            基于以上的一个场景,我们将展开一系列的分布式锁的了解,学习,深入实战过程。

    前置要求

    环境要求:jdk1.8、maven3.x

    技术要求:springboot2.x、mybatis、spring-data-redis、mysql、redis

    其他:zookeeper、lua脚本、juc等

     如果所示当存在多个线程并发访问同一个数据库资源时,可能会产生并发问题,例如超卖现象,在,在同一个服务中,我们可以采用jvm本地锁(synchronized锁或者ReentrantLock)来解决。但是jvm本地锁在三种情况下会导致失效。

    一、JVM本地锁失效的三种情况

    jmter并发测试统一采用如下模拟参数:

    100 用户量,每个用户量发送50次请求。

    • 多例模式

    springboot中默认为单例模式,我们可以在在service类上增加@Scope(value=“prototype”,proxyMode=ScopeProxyMode.TARGET_CLASS)注解 , 使该类成为cglib代理的多例模式。

    在多例模式下我们执行测试用例,发现产生了多卖现象,是因为获取的锁不是同一个对象锁导致。 

    • 事务

    在方法上增加@Transactional注解

     由于增加了事务注解,代码处理流程变更为开启事务,获取锁,查询库存,扣减库存,释放锁,提交事务。

    线程1线程2
    begin开启事务begin开启事务
    获取锁
    查询库存,库存数量为21
    扣减库存,库存数量为20
    释放锁
    (此时事务还未提交)获取锁

    查询库存,由于线程1事务还没提交,

    查询到的库存数量还是21

    扣减库存,这个时候线程1

    和线程2查询到的库存都是21,

    就会产生并发问题了。

    释放锁
    提交事务提交事务

    默认情况下,mysql的事务隔离级别是可重复读,REPEATABLE_READ,  所以线程2读取不到未提交事务的数据,也就是读取的还是线程1扣减库存之前的数据21,那么我们是否可以修改事务隔离级别来避免并发问题呢?答案是可以的。

    修改事务注解@Transactional(isolation=Isolation.READ_UNCOMMITTED)

     修改完成之后,再次执行测试用例,我们可以看到超卖现象已解决。原因是因为线程2查询到的是线程1还未提交的数据20。

    • 集群部署

    类似于多例模式,每一个服务中获取的锁,并不是同一个对象锁,这样自然就会产生并发问题导致多卖现象。相当于每个服务器中都会有一个stockService实例去查询和扣减库存。

    二、数据库悲观锁

    sql解决超卖的方式

    • 一个 update sql 语句:更新数量时增加库存数量>=1的判断,可以解决三种jvm锁失效的场景。

    update insert delete写操作本身就会加锁

    我们将代码改造为如下,只保留一个带有库存数量大于等于1判断的update操作。 

    update  stock  set  count= count -1 where  item_code = {itemCode} and count >= 1;

    但是一个更新sql语句也会有一定优缺点:

    (1)锁范围问题

    (2)同一个商品如果有多条不同仓库库存记录

    (3)先记录库存变化前后的状态

    上面的update sql 锁定的时候是采用的表级锁,锁定范围过大。

    mysql 悲观锁中使用行级锁:锁的查询或更新条件必须是索引字段,查询或者更新条件必须是具体值(=,in,不能是like,!= 这种查询条件)。

    • 使用 select ... for update 方式 

    select * from  stock where item_code = '1001' for update ;

    同样和update 一样需要注意锁定范围。但同样也存在一些问题,比如处理性能下降,死锁问题等,需要对多条数据加锁时,加锁顺序要一致,库存操作要统一,比如入库,出库,扣减库存都要使用select .. for update。

    • 乐观锁

    compare and swap 比较并交换

    例如我们可以在设计表的时候增加一列version 版本号字段或者时间戳字段来实现乐观锁。

    select * from db_stock where product_code = '1001';

    update db_stock set count = 4996, version = version+1 where id = 1 and  version = 0;

     在使用乐观锁的时候需注意两点,一是不能加事务注解,二是在更新失败的时候,防止无限的进栈出栈导致栈溢出,增加一个短暂睡眠。

     乐观锁存在的问题:高并发情况下,性能极低,ABA问题,读写分离情况下导致乐观锁不可靠

    三、mysql 锁总结

    性能: 一个update sql  > 悲观锁 > jvm锁 > 乐观锁

    如果追求极致性能,业务场景简单并且不需要记录数据前后变化的情况下,有限选择: 一个update sql。

    如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁

    如果写并发量较高,一般会经常冲突,此时如果选择乐观锁的话,会导致业务代码不间断的重试,优先选择: mysql悲观锁

    不推荐jvm本地锁。

  • 相关阅读:
    APS系统能消除造成生产和运输延迟的瓶颈
    14.HTML和CSS 02
    基于SSM的海鲜自助餐厅系统的设计与实现
    进程-死锁的概念/死锁产生的必要条件/死锁的处理策略/死锁的避免
    Unity—UGUI
    [附源码]计算机毕业设计springboot基于vuejs的爱宠用品销售app
    2022重庆自考本科怎么考?
    吴恩达深度学习测验题:deeplearning.ai-week1-quiz
    Python实现逻辑回归模型教程
    【云原生】手把手教你在arm64架构系统,安装kubernetes及适配kubevela
  • 原文地址:https://blog.csdn.net/qq_31905135/article/details/126624325