• 关于Mybaits缓存....


    记Mybaits缓存踩的坑

    1.问题提出

    最近开发一个记录操作前后修改内容的功能,获取修改前数据比较简单,直接从数据库获取,记录修改后的功能也比较简单,直接将用户修改的内容封装成po对象,然后两个比对就可以了,问题就出在这。

    2.场景复现

    下面是出现问题的代码,简化版(操作分为用户端和管理端):

    用户端

    public class UserServiceImpl{
      @Autowired
      private IUserDao userDao;
      
      @Autowired
      private IUserLogDao dao;
      
      @Autowired
      private StreamAction action;
      
      @Override
      @Transactional
      public void modifyUser(UserDto dto){
        Long id = dto.getUserId;
        //修改前的bean
        UserBean beforeBean = userDao.queryUser(id);
        
        //修改后的bean
        BeanUtils.copyProperties(dto,beforeBean);
        
        //更新和用户有关的内容
        //....用户信息填充 UserLogBean userLogBean
        
        //这里需要修改和用户关联的记录,所以插入一次
        userDao.insert(userLogBean);
        
        //用户端操作流
        action.request(beforeBean);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • StreamAction.request
    @Override
    @Transactional
    public void request(UserBean userBean){
      /**
      * 获取数据库原内容
      */
      UserBean afterBean = userDao.queryUser(id);
      
      //比对返回修改内容
      ModifyBean bean = modify(userBean,afterBean);
      
      //...其他 业务
      //插入修改后的内容
      userBeanModifyDao.insert(bean);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时去查看数据内添加的内容,可以看到,修改的部分被正确的比对出来了。

    管理端

    public class UserServiceImpl{
      @Autowired
      private IUserDao userDao;
      
      @Autowired
      private IUserLogDao dao;
      
      @Autowired
      private StreamActionManager action;
      
      @Override
      @Transactional
      public void modifyUser(UserDto dto){
        Long id = dto.getUserId;
        //修改前的bean
        UserBean beforeBean = userDao.queryUser(id);
        
        //修改后的bean
        BeanUtils.copyProperties(dto,beforeBean);
        
        //更新和用户有关的内容
        //....用户信息填充 UserLogBean userLogBean
        
        //管理端操作流
        action.request(beforeBean);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • StreamActionManager.request
    @Override
    @Transactional
    public void request(UserBean userBean){
      /**
      *   获取数据库原内容
      */
      UserBean afterBean = userDao.queryUser(id);
      
      //比对返回修改内容
      ModifyBean bean = modify(userBean,afterBean);
      
      //...其他 业务
      //插入修改后的内容
      userBeanModifyDao.insert(bean);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时我们去数据库查看内容,你就惊奇发现并没有见到修改的内容。不对啊,我们确实已经修改过了,那为什么数据库里不显示呢?

    3.发现问题

    带着疑问,我们很容易的想到,是不是两个对象内容一模一样?带着疑问,我们尝试着打印一下用户端和管理端,在进行比对前的两个对象内容:

    public void request(UserBean userBean){
      /**
      * 获取数据库原内容
      */
      UserBean afterBean = userDao.queryUser(id);
      
      System.out.println(userBean);  
      System.out.println(afterBean); 
      //比对返回修改内容
      ModifyBean bean = modify(userBean,afterBean);
      
      //......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    用户端

    我们在比对修改内容前打印两个bean,结果如下:

    cn.example.core.core.bean.UserBean@5e5a2b74
    cn.example.core.core.bean.UserBean@5e5a2b75
    
    • 1
    • 2

    两个bean不是同一个对象,符合我们的预期,所以用户端正确入库。

    我们再来看管理端

    管理端

    管理端执行结果

    cn.example.core.core.bean.UserBean@5e5a2b77
    cn.example.core.core.bean.UserBean@5e5a2b77
    
    • 1
    • 2

    !!!oi!!!,我们惊奇的发现,这两个对象是一样的,那就奇了怪了,用户端和管理端业务代码甚至基本都一样的,为什么会造成这个原因呢?我们再输出这两个对象的内容

    System.out.println(userBean.toString());  
    System.out.println(afterBean.toString());
    
    • 1
    • 2

    很惊奇的是,这两个对象内容都是修改过的内容,也就是service内通过BeanUtils属性赋值过的内容,那我们mysql里的内容去哪了??我们明明还没更新啊!我们赶紧去数据库看一眼,发现数据库里并没更新。数据库里内容没更新,业务里的bean已经被更新过了,这是为什么?

    4.排查问题

    我们找到出现问题的原因了,是因为管理端两个对象一样。那为什么会一样呢?

    我们在业务逻辑里很容易想到类似的场景,比如我们在使用redis的时候,当redis内有数据时,我们希望走redis返回结果而不是走数据库,以提高查询性能,那会不会两个对象一样也是走了缓存呢?我们通过查询数据库我们知道,mysql的查询也会存在缓存,但是按道理来说,我们最后的结果应该是mysql的内容,应该不会是后面的内容。所以只有一种情况,是mybaits的缓存

    5.寻找答案

    我们已经确定了是mybaits的缓存导致的问题,但是为什么管理端和用户端还不一样呢?为什么用户端就没有这个问题呢?

    我们百度之后发现,mybaits有一、二级缓存之分,二级缓存默认不开启。

    哎会不会是用户端的缓存过期了?因为用户端的有一个插入其他表的操作,肯定比管理端慢,对对一定是这个问题,好我们去找度娘,度娘说缓存没有过期时间。好好好这样玩,好好好。

    那么问题是什么?我们已经确定不是过期时间的问题了,那我们现在想的就是缓存过期,也就是缓存失效了,我们换个方法去查找内容,“mybatis缓存失效的原因”,我们找到以下结果:

    1.不在同一个sqlSession中
    2.如果是增删改操作,程序会clear缓存。
    3.一级缓存未开启
    4.手动清空缓存数据,调用sqlsession.clearCache().
    5.更改查询条件
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们一点点往下看:

    1. 不在同一个sqlSession中

    同一个事务内会复用同一个sqlSession。

    具体我们查看控制台,可以看到第一个sql执行之前,会有一句话

    Creating a new SqlSession
    Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3663af34]
    
    • 1
    • 2

    在之后的sql执行时会有一句话

    Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3663af34] from current transaction
    
    • 1

    都是同一个sqlSession

    我们业务层面的代码是在同一个事务里,因为没有设置事物传播机制,虽然有两个事物注解,但是最后都在同一个事务那,可以用

    String txName = TransactionSynchronizationManager.getCurrentTransactionName();
    
    • 1

    查看,会发现事务没有失效且都在一个事物内,显然不是这个原因。

    1. 如果是增删改操作,程序会clear缓存。

    我们用户端涉及的增删改是另外一张表的,排除

    1. 一级缓存未开启

    显然开启了,不然不会有这篇博客

    4、5不用看了,都不符合

    我们分析完了,仍然没找到原因。那么问题到底在哪呢?

    目前最有可能的就是2,但是我们明明更改的是其他表啊,那么不妨,我们就试试改其他表。

    @Test
    @Transactional  //这里的事务一定要加,mybatis的缓存存在条件就是需要有事务,否则你查询会发现两个对象怎么样都不会相同
    public void test(){
      UserBean beforeBean = userDao.queryUser(id);
      
      //更新
      userLogDao.update("1","1");
      
      UserBean afterBean = userDao.queryUser(id);
      
      System.out.println(beforeBean);  
      System.out.println(afterBean); 
      
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    哎,神奇,两个对象不一样了,缓存失效了!所以问题就在这,所以这就是造成这个bug的原因,知道了问题,那我们就开始做解决方案

    6.解决方案

    解决方案有一下几种:

    • 关闭mybatis一级缓存,无法关闭,只能修改状态
    mybatis:
      configuration:
        cache-enabled: false #禁用二级缓存
        local-cache-scope: statement #一级缓存指定为statement级别 默认为session级别
    
    • 1
    • 2
    • 3
    • 4
    • 使用不同的对象传递

    在传递前后bean的时候,用其他的bean赋值传递。

    • 使用不同的查询方式,拼接条件等
    • 使用不同的事物隔离级别,sqlSession是依赖于mysql的事物,所以如果数据库不支持事物那么Spring的事物 也不会生效。我们可以使用不同的事物隔离级别,以创建不同的sqlSessio,此时就不存在bean不同的问题:
      @Override
      @Transactional
      public void modifyUser(UserDto dto){
        Long id = dto.getUserId;
        //修改前的bean
        UserBean beforeBean = userDao.queryUser(id);
        
        //修改后的bean
        BeanUtils.copyProperties(dto,beforeBean);
        
        //更新和用户有关的内容
        //....用户信息填充 UserLogBean userLogBean
        
        //管理端操作流
        action.request(beforeBean);
      }
    
    	//另外一个类的request方法
    	@Override
    	@Transactional(propagation = Propagation.REQUIRES_NEW)
    	public void request(UserBean userBean){
    	  /**
    	  *   获取数据库原内容
    	  */
    	  UserBean afterBean = userDao.queryUser(id);
    	  
    	  //比对返回修改内容
    	  ModifyBean bean = modify(userBean,afterBean);
    	  
    	  //...其他 业务
    	  //插入修改后的内容
    	  userBeanModifyDao.insert(bean);
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    使用不同的事物传播机制,我们可以看到控制台创建了两个sqlSession:

    Creating a new SqlSession
    Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7488c183]
    
    //....
    Creating a new SqlSession
    Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4797023d]
    
    //再次比较两个bean
    cn.example.core.core.bean.UserBean@4b86a656
    cn.example.core.core.bean.UserBean@4c3c31a5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    到此为止,我们的问题就解决了。

  • 相关阅读:
    雅可比矩阵和雅可比坐标
    UE学习记录06----根据Actor大小自适应相机位置
    SpringBoot 刷新上下文1--主流程
    如何不编写 YAML 管理 Kubernetes 应用?
    八、Nacos服务注册和配置中心
    Redis常见面试题
    【无标题】
    postgresql进行getshell
    【C++初阶】类和对象(二)
    ViewPage2+TabLayout地表超详总结
  • 原文地址:https://blog.csdn.net/YSecret_Y/article/details/133776009