首页 > 数据库 >序列号生成并发引发的synchronized、数据库隔离级别、myabits缓存等一些问题记录

序列号生成并发引发的synchronized、数据库隔离级别、myabits缓存等一些问题记录

时间:2023-02-23 09:57:35浏览次数:32  
标签:codeRuleInfo 事务 缓存 synchronized 数据库 myabits 序列号 select

起因

  • 一个序列号产生方法发现有并发问题。
  • 修改这个方法中发生了一些错误,而这涉及到了一些的知识点,所以记录下。

涉及点

  • synchronized方法:如果此方法内包含数据库操作,且外围有事务时,并不能完全锁住。
  • 数据库隔离级别
    • RC-不可重复读,也就是在同一个事务中,多次select同一条sql获取的结果可能不同。
    • RR-可重复度,同一个事务中,多次select同一条sql获取的结果相同,除非中间update进行了数据修改。
    • mysql的默认隔离级别是RR,其它的数据库一般默认级别是RC。
  • mybatis缓存,在有事务的情况下,多次select同一个sql,第二次将不再请求数据库,而是直接从缓存读取。

描述

情景一
  • 类似的代码如下,下发的代码在并发时就会出现重复的code:

    public Integer getCode(){
        CodeRule codeRuleInfo = selectCodeRuleInfo();//1、从数据库获取当前的记录
        Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1
        codeRuleInfo.setCode(newCode);//2、赋值最新的
        updateCodeRule(codeRuleInfo);//3、更新数据库
        return newCode;
    }
    
  • 由于是单机环境,最开始就想用synchronized解决,但发现getCode本身是在一个事务下的,这样就导致仅仅靠synchronized并不能锁住,原因如下:

    1-事务开始 -> 2-synchronized开始 -> 3-synchronized结束 -> 4-事务提交
    

    其中3和4之间会又时间差,导致这期间如果有并发进来,还是会出现重复,所以需要改成:

    1-synchronized开始 -> 2-事务开始 -> 3-事务提交 -> 4-synchronized结束 
    
  • 正常的改进应该是:

    //原有方法,增加synchronized锁
    public synchronized Integer getCode(){
        return AAA.getCode();
    }
    
    //另一个类中AAA,开启新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer getCode(){
        CodeRule codeRuleInfo = selectCodeRuleInfo();
        Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1
        codeRuleInfo.setCode(newCode);
        updateCodeRule(codeRuleInfo);
        return newCode;
    }
    
情景二
  • 当时改的时候脑子抽了,并没有按照上方的改,而是改成这样了:

    //原有方法,增加synchronized锁
    public synchronized Integer getCode(){
        CodeRule codeRuleInfo = selectCodeRuleInfo();
        Integer newCode = codeRuleInfo.getCurrentCode() + 1;//当前code+1
        codeRuleInfo.setCode(newCode);
        AAA.updateCodeRule(codeRuleInfo);//只把更新放到了新事务中
        return newCode;
    }
    
    //另一个类中AAA,开启新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer updateCodeRule(){
        //执行sql更新
    }
    
  • 上述方法就导致一个问题,select方法在主事务中,而update方法在新事务中,部署到线上果然不但没解决,反而非并发仅仅是循环中就全部重复了,原因如下:

    • mysql默认的隔离级别是RR,也就是同一个事务内多次select同一个语句,获取的结果都是一样的,除非两个select之间有update操作才会重新获取最新数据。但我们的update方法放在了新的事务中,导致并没有对select产生影响,所以循环下的getCode就会产生重复数据。
  • 但是诡异的是,我们在部署前有作过简单的测试,测试是正常递增的,找了半天才发现是mybatis缓存的原因:

    • mybatis一级缓存会对多次相同的sql查询进行缓存,第二次就不会再读取数据库了,而是直接从缓存中获取,除非中间进行了数据库的增删改,而新事务中增删改同样不影响缓存清除,听起来和数据库的RR隔离级别很像,但这里有个本质区别,它缓存的是一个对象,如果你对这个对象中的数据进行了修改,那么下次获取的是修改后的数据,而我们代码中每次都有codeRuleInfo.setCode(newCode);来对此对象进行了修改,所以在mybatis缓存的作用下,取出的数据却是正常递增的。
  • 这样就又不对了,照理来说如果按照mybatis缓存,那正式环境也不应该会重复,再进一步的排查,原因如下:

    • 我们测试环境中的很简单,就一个select和一个update,但我们正式环境中getCode仅仅是一个主事务中的一个小方法,在循环中getCode后面还有一些其它表的增删改操作,而这些增删改会清空mybatis的缓存。
    • 这里我们有一个误区,原以为mybatis缓存也是仅仅只有针对该表的增删改才会清除缓存,但实际上是针对任意表的增删改都会清除缓存,所以我们线上环境是每次都会清缓存从数据库读取,然后就是数据库RR隔离级别导致数据重复,而本地测试则是走缓存,正常增加。

更好的并发处理

  • 这种涉及到数据库又存在事务的,采用synchronized方法还是有些缺陷的:子事务独立于主事务,如果子事务出问题,则不能整体回滚了。感觉更应该考虑用数据库的悲观锁和乐观锁来解决,或者是参考微服务的一些锁实现。
  • 悲观锁实现比较简单点,在并发不高时可以考虑,在select 语句中加上for update,但需要注意的是在RR隔离环境下,如果select没有命中数据,可能会产生间隙锁(很复杂,不同主键、索引、非索引造成的锁类型各不相同,可自行搜索相关资料),此时又insert就可能会造成死锁。解决方法是设置数据库隔离级别为RC,因为RC模式下不会有间隙锁产生,从而不会死锁。
  • mysql数据库设置成RC隔离级别也没什么问题,毕竟OraclePostgreSQL等一大众数据库的默认级别就是RC,而阿里云等一些云厂商的RDS数据库隔离级别默认也是RC

标签:codeRuleInfo,事务,缓存,synchronized,数据库,myabits,序列号,select
From: https://www.cnblogs.com/vishun/p/17146812.html

相关文章

  • mysql 数据库 序列号 自增长
    在同一个节点任何一个数据库上新建MYCAT_SEQUENCE表CREATETABLEMYCAT_SEQUENCE(nameVARCHAR(50)NOTNULLCOMMENT'sequence名称',current_valueINTNOTNULLCO......
  • 【多线程与高并发】- synchronized锁的认知
    synchronized锁的认知......
  • synchronized关键字
    1、当一个线程进入一个对象的一个synchronized方法后,其他线程可以进入此对象的非同步方法,不可进入此对象此同步方法,也不可进入此对象其他同步方法。同步监视器的意思是:线程......
  • Synchronized和Volatile的对比
    Synchronized和Volatile是并发中的两大关键字,有相似性和不同点。Synchronized更详细介绍参考https://www.cnblogs.com/spark-cc/p/17069585.htmlvolatile简单来说就是轻......
  • java synchronized wait notifyAll Thread
    虚假唤醒ifpackagepc;importjava.util.ArrayList;importjava.util.List;importjava.util.Random;publicclassD1{publicstaticvoidmain(String[]args){......
  • Java并发JUC——synchronized和Lock
    synchronizedsynchronized作用原子性:synchronized保证语句块内操作是原子的。可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实......
  • volatile和synchronized关键字
    1.volatile关键字 Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这......
  • 序列号相关总结和学习
    摘要最近有多个项目出现过因为序列号导致系统吞吐量上不去性能下降的情况.晚上想着学习总结一下,已备忘,避免后续继续掉坑里.学习资料来源:https://cdn.modb.pro/......
  • java多线程基础小白指南--synchronized同步块
    sychronized是java多线程非常关键的一个知识点,这篇博客将从synchronized几个用法以及代码来学习。sychronized的作用是能够保证同一时间只有一个线程来运行这块代码,达到并......
  • 浅谈synchronized关键字的理解
    简介:synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized属于独占......