• 随笔感悟:Mysql悲观锁和乐观锁


    目录

    首先是场景:并发控制

    悲观锁

     select ... for update的使用

     乐观锁

    解释:

    特点:

    实现:

    sql演示例子:

     结合我们的Java代码的实现

    以上更新语句存在一个比较严重的问题,即ABA问题:

    解决方式:

    优化


     

    首先是场景:并发控制

     为什么要使用悲观锁和乐观锁——>为了并发情况下,线程跟自己在单机情况一样得到相同的结果,保证数据的一致性;

    没做好并发控制就会可能出现:脏读、不可重复读、幻读等问题;

    结论:乐观锁适合并发量比较小的场景(读多写少),悲观锁适合并发量比较大的场景(写多多少);

     

    悲观锁

    目的:在并发时保证线程和单机时结果一样(宏观);当在数据库中修改某条数据时,防止同时被其他人进行修改从而造成数据不一致(微观);——>(处理:加锁防止并发,利用锁,线程锁住资源防止被其他人修改);

    场景:有点像警察处理凶杀现场,对凶杀现场(资源对象)进行上锁,防止其他人对其现场进行修改;

    特点:具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度

    实现:像我们的表锁、行锁都是悲观锁,在java中就是synchronized关键字实现

     然后我们悲观锁主要分为共享锁排他锁

    • 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
    • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。(比如行锁和表锁)

    例子:

    1.首先事务1先开启事务——>2.然后事务2开启事务,事务1修改id=10001的数据——>3.然后事务2修改同样为id=10001的数据发现阻塞——>结论:验证了id=10001被行锁锁住了(如果没有索引的话就是表锁)

     

    5. 最后事务1回滚,释放锁资源,事务2又可以执行了

     select ... for update的使用

      1.事务1先开启事务查询id=10001的数据——>2.事务2开启查询事务1一样的数据发现阻塞——>3.然后事务1回滚或者commit即可

     乐观锁

    解释:

    乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候(重点),才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量

    特点:

    1.更加宽松,性能较好,吞吐量较高,有效避免了死锁等现象

    2.但是可能会出现cpu空转的现象,造成cpu飙高

    可能会出现线程一直retry,并且ABA问题也会出现,中途被其他线程改了比较的version,然后又改回来,中途做了其他事情

    实现:

    1.CAS实现

    2.版本号控制,一般像我们mysql中在表中加一个字段version版本号:表示数据被修改的次数;当数据被修改时,verson+1;在线程更新时,在读取数据的时候也要读取version,在提交更新时,若刚才读取到的version值和当前数据库中的version值相等才更新,否则retry;

    sql演示例子:

    当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如前面的扣减库存问题,通过乐观锁可以实现如下

    1. //1.查询数据的sql
    2. select * from tb_item_stock where item_id=10001;
    3. //2.修改数据的sql
    4. update tb_item_stock set stock=stock+1,version=version+1 where item_id=10001 and version=1;

    事务1:先select一下看下版本,下面图片是事务1第二次select(也就是在事务2提交更新sql后的一个操作)

     事务2:

     结合我们的Java代码的实现

    1. /**
    2. * 模拟多版本号
    3. */
    4. @Transactional
    5. public void deduct2(){
    6. //1.首先查询仓库里面的内容
    7. List stocks = this.stockMapper.queryStock("10001");
    8. //获取第一条仓库记录
    9. Stock stock = stocks.get(0);
    10. //2.判断仓库是否充足
    11. if(stock!=null&&stock.getStock()>0){
    12. //2.1扣减库存,然后设置新的版本号进行更新
    13. stock.setStock(stock.getStock()-1);
    14. Integer version = stock.getVersion(); //获取当前商品版本
    15. stock.setVersion(version+1); //更新一次版本加一次,如果中途有其他的更新操作就会失败
    16. //2.2然后进行更新——>需要进行判断如果更新影响行数为0就进行cas操作
    17. if(this.stockMapper.update(stock,new UpdateWrapper()
    18. .eq("item_id",stock.getItemId())
    19. .eq("version",version))==0){
    20. this.deduct2();//cas
    21. }
    22. }

    以上更新语句存在一个比较严重的问题,即ABA问题

    1. 比如说线程一从数据库中取出库存数 3,这时候线程二也从数据库中取出库存数 3,并且线程二进行了一些操作变成了 2。
    2. 然后线程二又将库存数变成 3,这时候线程一进行 CAS 操作发现数据库中仍然是 3,然后线程一操作成功。
    3. 尽管线程一的 CAS 操作成功,但是不代表这个过程就是没有问题的。

    解决方式:

    我们可以加一个时间戳,因为时间戳天然具有顺序递增性;

    还一个场景,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度。一个比较好的建议,就是减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!

     

     比如我们的AutomicInteger就是利用cas

    1. public final boolean compareAndSet(int expectedValue, int newValue) {
    2. return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    3. }

    优化

    LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:

     LongAdder 核心思想就是热点分离,这一点和 ConcurrentHashMap 的设计思想相似。就是将 value 值分离成一个数组,当多线程访问时,通过 hash 算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度

     (41条消息) LongAdd_Fairy要carry的博客-CSDN博客_longadd

  • 相关阅读:
    vue3探索——pinia高阶使用
    【Spring】——11、了解BeanPostProcessor后置处理器
    主键、外键、建表范式、MySQL索引、用户管理
    前端性能优化:如何根据chrome的timing优化
    LeetCode 278. 第一个错误的版本
    frp穿透你的远程桌面
    【已解决】Unity Coroutinue 协程未有效执行的问题
    R语言 pca主成分分析的主要方法
    python解析pcap报文_scapy解析pcap文件
    Git的使用
  • 原文地址:https://blog.csdn.net/weixin_57128596/article/details/126821663