mybatis开启二级缓存时脏数据的问题(mybatis 二级缓存)

springboot,mybatis-plus项目进行中,框架的复杂给缓存也带来了很大问题,粗粒度的缓存控制总感觉不能满足需求,精准控制。所以寻求开启mybatis二级缓存或者开启springboot业务service层的缓存;

开启mybatis二级缓存呢,网上文章不少,

1)需要开启mybatis-plus.configuration.cache-enabled: true;

2)需要编写cache扩展接口类,我的是MybatisRedisCache;(redis扩展呢,需要额外redis的数据源配置)

3)需要在mapper类上加注解@CacheNamespace或者xml上加<cache>标签,@CacheNamespaceRef或者<cache-ref>标签可以实现引用两一个mapper的缓存空间,即能实现多表共用一个缓存空间,可以控制多表查询时脏数据出现。

我的MybatisRedisCache类如下:

/**
 * 实现mybatis-plus 二级缓存配置<br>
 * 需要使用@CacheNamespace(implementation = MybatisRedisCache.class),<br>
 * 在mapper类上加注解 @CacheNamespace,注意key及value的哈希算法
 * */
public class MybatisRedisCache implements Cache {
	private static final Logger log = LoggerFactory.getLogger(MybatisRedisCache.class);

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate<Object, Object> redisTemplate;

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        log.debug("MybatisRedisCache.id={}", id);
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        try {
            log.debug("MybatisRedisCache.putObject: {}->{}->{}", this.id, key, value);
            getRedisTemplate().opsForHash().put(this.id, key.toString(), value);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

    @Override
    public Object getObject(Object key) {
        try {
            Object hashVal = getRedisTemplate().opsForHash().get(this.id.toString(), key.toString());
            log.debug("MybatisRedisCache.getObject: {}->{}->{}", id, key, hashVal);
            return hashVal;
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * 该方法由于mybatis本身存在缺陷,sqlsession在更新数据时在有先执行的查询语句而导致更新后有脏数据时会回滚,并调用次方法,所以也必须清空整个mapper缓存
     * */
    @Override
    public Object removeObject(Object key) {
    	clear();
//        try {
//            getRedisTemplate().opsForHash().delete(this.id.toString(), key.toString());
//            log.debug("MybatisRedisCache.removeObject: {}->{}->{}", id, key);
//        } catch (Exception e) {
//            log.error(e.getMessage());
//        }
        return null;
    }

    @Override
    public void clear() {
        try {
        	getRedisTemplate().delete(this.id.toString());
            log.debug("MybatisRedisCache.clear: {}", id);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

    @Override
    public int getSize() {
        try {
            Long size =   getRedisTemplate().opsForHash().size(this.id.toString());
            log.debug("MybatisRedisCache.getSize: {}->{}", id, size);
            return size.intValue();
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return 0;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    private RedisTemplate<Object, Object> getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = SpringContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }

}

当前呢,先调控mybatis二级缓存,发现了个问题,就是当有的mapper执行删除编辑时会调用MybatisRedisCache.clear()方法清空缓存,而有个别mapper执行删除编辑时,却不能调用MybatisRedisCache.clear(),只调用了MybatisRedisCache.removeObject(Object key)方法;导致脏数据;

TransactionalCache是事务模式mybatis二级缓存管理工具,

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
            + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
      }
    }
  }

}

service业务层我开启了事物,调试的时候发现正常清空mapper缓存调用的方法是TransactionalCache.commit()方法里的delegate.clear();delegate代表的就是MybatisRedisCache类,不正常的时候调用的TransactionalCache.rollback()方法。

没办法,我只好改了MybatisRedisCache.MybatisRedisCache.removeObject(Object key)方法,改成调用clear()方法.

避免脏数据呢,最好是maper类之间有良好的规划,尽量少用联表查询,改用关联表mapper查询作为中间步骤;如果非要用联表查询,最好加注:@Options(useCache = false)标记关闭缓存

例如:

    /**
     * 查询参数包含groupId,需要联表查询 username,truename,nickname,phone,comment,mail
     * */
    @Options(useCache = false)
    @Select("<script>"
    		+ "SELECT u.* FROM `sys_user` u "
    		+ "<if test = \"groupIds!=null and groupIds.size()>0\" > "
    			+ " left outer join sys_group_user g on u.id=g.uId "
    		+ "</if>"
    		+ "<if test = \"roleId!=null\" > "
				+ " left outer join sys_role_user r on u.id=r.uId "
			+ "</if>"
			+ "<where>"
    		+ "<if test = \"blurry!=null and blurry!=''\" >"
    		+ " AND ("
	    		+ "u.username LIKE concat('%', #{blurry}, '%') "
	    		+ "or u.truename LIKE concat('%', #{blurry}, '%') "
	    		+ "or u.nickname LIKE concat('%', #{blurry}, '%') "
	    		+ "or u.phone LIKE concat('%', #{blurry}, '%') "
	    		+ "or u.comment LIKE concat('%', #{blurry}, '%') "
	    		+ "or u.mail LIKE concat('%', #{blurry}, '%') "
    		+ ")"
    		+ "</if>"
    		+ "<if test = \"enabled!=null\" > AND u.enabled = #{enabled} </if>"
    		+ "<if test = \"groupIds!=null and groupIds.size()>0\" > "
	    		+ "AND g.groupId in "
	    		+ "<foreach collection=\"groupIds\" index=\"index\" item=\"item\" open=\"(\" separator=\",\" close=\")\">#{item}</foreach>"
    		+ "</if>"
    		+ "<if test = \"roleId!=null\" > "
				+ " AND r.roleId=#{roleId} "
			+ "</if>"
    		+ "<if test = \"createtime !=null and createtime.size()>1\" > "
    			+ " AND u.createtime BETWEEN  #{createtime[0]} and #{createtime[1]}"
    		+ "</if>"
    		+ "</where>"
    		//+ " order by u.id asc"
    		+ "</script>")
    List<User> queryWithGroupIds(UserFilter filter);

如有朋友有更好的建议请留言,谢谢!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注