Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!

先说两句:

  我们都知道Mybatis缓存分两类: 一级缓存(同一个Session会话内) & 二级缓存(基于HashMap实现的以 namespace为范围的缓存)

  今天呢, 我们不谈一级缓存, 我们来谈一谈 二级缓存, 通过查看Mybatis源码发现, 他的二级缓存实现真的十分简单, 默认的实现类是 org.apache.ibatis.cache.impl.PerpetualCache 这里贴一下他的源码吧:

  

/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

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

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

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

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

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

PerpetualCache.java

  那么既然他都已经有一个实现了, 我们为什么还要自定义实现呢?

  原因很简单, 默认实现是(HashMap)本地缓存, 不支持分布式缓存, 而我们现在大多数项目都是以集群的方式部署, 这种情况下, 使用本地缓存会出现很严重的脏读问题, 特定情况下更新可直接导致数据不一致的问题.

  如果让缓存实现支持分布式呢? 方案有很多, 基本围绕着NoSQL数据库实现, 最常用的就是Redis了, 接下来我们就来用Redis实现自定义缓存类

  说干就干吧, 开始我们的自定义缓存实现之路

实现过程:

  首先: Mybatis自定义实现缓存类的配置方式十分简单, 我们只需要开启二级缓存, 并在 mapper.xml 中修改如下配置:

  

  其中 type 属性值就是我们自定义的缓存实现辣!

  接下来我们来看一看实现类内部是怎么写的:

  

package com.cardgame.demo.game.component.db;

import com.cardgame.demo.component.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;

/**
 * @author yjy
 * 2018-08-06 15:46
 */
@Slf4j
public class MybatisRedisCache implements Cache {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private String id;

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("id can not be null");
        }
        log.debug("MyBatisRedisCache:id=" + id);
        this.id = id;
    }

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

    @Override
    public int getSize() {
        int size = (int) RedisUtils.getInstance().getHashSizeObj(getKey());
        log.debug("MybatisCache getSize > {}", size);
        return size;
    }

    @Override
    public void putObject(Object key, Object value) {
        RedisUtils.getInstance().setHashObj(getKey(), key, value);
        log.debug("MybatisCache put > key: {}, val: {}", key, value);
    }
    @Override
    public Object getObject(Object key) {
        Object val = RedisUtils.getInstance().getHashObj(getKey(), key);
        log.debug("MybatisCache get > key: {}, val: {}", key, val);
        return val;
    }

    @Override
    public Object removeObject(Object key) {
        // 移除指定缓存
        Object obj = RedisUtils.getInstance().getHashObj(getKey(), key);
        RedisUtils.getInstance().delHashObj(getKey(), key);
        log.debug("MybatisCache remove > key: {}, val: {}", key, obj);
        return obj;
    }

    @Override
    public void clear() {
        // 清除所有缓存
        RedisUtils.getInstance().delObj(getKey());
        log.debug("MybatisCache clear > hashKey : {}", getKey());
    }

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

    protected String getKey() {
        return REDIS_KEY_GAME + "mybatis_cache_" + id;
    }

}

MybatisRedisCache

  自定义缓存实现的要求很低, 只需要 实现 org.apache.ibatis.cache.Cache 就可以了. 这里我们用到了 RedisUtils 工具类, 还请同学们 对应到自己的项目中的工具类.

  至此: 我们的Mybatis二级缓存就支持分布式啦!

问题来临:

  今天我偶然想起, 是否需要给二级缓存 设置一个过期时间?

  我们来想一下不设置过期时间会有什么问题:

    当有一天我们不得不手动修改数据库的数据时(别问为啥要动数据库, 因为我在本地测试需要修改), 如果相应的 namespace 没有 插入和更新操作, 那么他的缓存将一直有有效, 然后查出来的数据一直是修改前的数据, 而且我们使用的Redis做的缓存, 即使重启了系统缓存依然还是在, 只能从redis中找到指定缓存并清除, 实在是头疼

  有没有办法解决呢?

  你可能已经想到了, cache标签不是支持 flushInterval 属性的吗? 设置一个 flushInterval = "10000", 这样不就行了吗?

  我开始也是这么认为的, 直到有一天我发现这个设置完全没有起作用, 缓存一直是有效的, 那问题就来了, 为什么呢?

  百思不得其解的我决定去瞄一瞄Mybatis的源码, 最终让我发现了其中的奥秘, 我们来看一下下面的代码:

  

  这是Mybatis初始化二级缓存中的一段代码, 我们可以看到, flushInterval 属性对于自定义实现类是不起作用的, 而 Mybatis 实现的缓存过期时间的原理则是利用 设计模式(装饰器模式) 在默认的 缓存实现类上封装了一层 ScheduleCache, 也正是此类实现了缓存的有效期设置.

  完了完了, 那不能设置咋办啊, 这不是坑爹吗?

  冷静下来, 先别忙. 既然 不能通过设置解决, 那我们就自己想办法解决吧

  既然你Mybatis不帮我封装 ScheduleCache, 那我就自己封装一个 ScheduleRedisCache, 原有的MybatisRedisCache不变. 我们看一下 ScheduleRedisCache的代码:

  

package com.cardgame.demo.game.component.db;

import com.cardgame.demo.component.redis.RedisUtils;
import org.apache.ibatis.cache.Cache;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME;

/**
 *
 * 二级缓存 过期时间 实现
 *
 * @author yjy
 * 2018-08-13 13:21
 */
public class ScheduleRedisCache implements Cache {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private String id;
    private long flushInterval = 10000; // 缓存刷新时间, 单位毫秒
    private Cache delegate; // 委派缓存类

    public ScheduleRedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("id can not be null");
        }
        this.id = id;
        this.delegate = new MybatisRedisCache(id);
    }

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

    @Override
    public void putObject(Object key, Object value) {
        // 记录过期时间
        long timeout = System.currentTimeMillis() + flushInterval;
        RedisUtils.getInstance().setHashObj(getKey(), key, timeout);
        delegate.putObject(key, value);
    }

    @Override
    public Object getObject(Object key) {
        Object timeout = RedisUtils.getInstance().getHashObj(getKey(), key);
        // if 未过期
        if (timeout != null && (long) timeout > System.currentTimeMillis()) {
            // 更新过期时间
            RedisUtils.getInstance().setHashObj(getKey(), key, System.currentTimeMillis() + flushInterval);
            return delegate.getObject(key);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        RedisUtils.getInstance().delHashObj(getKey(), key);
        return delegate.removeObject(key);
    }

    @Override
    public void clear() {
        RedisUtils.getInstance().delObj(getKey());
        delegate.clear();
    }

    @Override
    public int getSize() {
        return (int) RedisUtils.getInstance().getHashSizeObj(getKey());
    }

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

    protected String getKey() {
        return REDIS_KEY_GAME + "schedule_cache_" + id;
    }

}

ScheduleRedisCache.java

  代码简单易懂, 说白了就是增加一个 缓存管理, 专门 缓存二级缓存的 过期时间. 在适当的时候 更新过期时间 / 清除过期时间

  需要注意的是, 既然我们封装了 MybatisRedisCache, 那么 mapper.xml 中就需要 改一改了, 如下:

  

  

  这样, 我们就实现了 二级缓存的超时自动过期功能了!!!

结论: 谢谢大家!666

  

  

原文地址:https://www.cnblogs.com/imyjy/p/9468186.html

时间: 2024-10-08 19:50:41

Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!的相关文章

【MyBatis学习13】MyBatis中的二级缓存

1. 二级缓存的原理 前面介绍了,mybatis中的二级缓存是mapper级别的缓存,值得注意的是,不同的mapper都有一个二级缓存,也就是说,不同的mapper之间的二级缓存是互不影响的.为了更加清楚的描述二级缓存,先来看一个示意图: 从图中可以看出: sqlSession1去查询用户id为1的用户信息,查询到用户信息会将查询数据存储到该UserMapper的二级缓存中. 如果SqlSession3去执行相同 mapper下sql,执行commit提交,则会清空该UserMapper下二级缓

Mybatis使用Redis二级缓存

在Mybatis中允许开发者自定义自己的缓存,本文将使用Redis作为Mybatis的二级缓存.在Mybatis中定义二级缓存,需要如下配置: 1. MyBatis支持二级缓存的总开关:全局配置变量参数“cacheEnabled=true” 2.select语句所在的Mapper需配置了<cache> 或<cached-ref>节点 3.select语句的参数 useCache=true Mybatis配置文件如下: <settings> <!-- 这个配置使全局

Mybatis学习笔记-二级缓存

~ 缓存是提供应用系统性能的重要手段,前面已经介绍过Mybatis是一级缓存,与一级缓存不同的是,二级缓存的作用范围扩大到了应用级别,并且可以比一级缓存更细粒度的对二级缓存的应用进行配置.下面就详细叙述一下Mybatis的二级缓存实现. 二级缓存的工作流程 不多说,先上图,图解是攻城狮最好的朋友^_^- 前面在叙述一级缓存的时候已经说过,一级缓存是一块放在Executor中的map内存块.试想一下,如果二级缓存也是Executor中的内存块可以可以呢?-- 当然不可以!为啥?因为二级缓存要支持细

SpringBoot+Mybatis+redis实现二级缓存

对于查询比较多的项目可以考虑配置二级缓存,mybatis本身的二级缓存是缓存到本地,但是对于多个节点的项目来说,可能会出现数据不一致的问题,所以采用redis缓存,这样二级缓存的数据就可以缓存到内存,可实现多个节点项目的数据同步. 1.配置redis的连接 #redis gmall.redis.host=172.16.1.250 gmall.redis.port=6379 gmall.redis.pass=Gworld2017 gmall.redis.photo.database=6 #最大分配

Mybatis一级、二级缓存

一级缓存 首先做一个测试,创建一个mapper配置文件和mapper接口,我这里用了最简单的查询来演示. <mapper namespace="cn.elinzhou.mybatisTest.mapper.UserMapper"> <select id="findUsers" resultType="cn.elinzhou.mybatisTest.pojo.User"> SELECT * FROM user </se

mybatis(4)_二级缓存深入_使用第三方ehcache配置二级缓存

增删改对二级缓存的影响 1.增删改也会清空二级缓存 2.对于二级缓存的清空实质上是对value清空为null,key依然存在,并非将Entry<k,v>删除 3.从DB中进行select查询的条件是: 1.缓存中根本不存在这个key 2.存在key对应的Entry,但是value为null 二级缓存的配置 <cache eviction="FIFO" flushInterval="60000" size="512" readOn

mybatis+redis实现二级缓存

在网上看了很多资料,发现例子都是千篇一律的相互复制.而且,使用的都是jedis的客户端..我这里使用的是redistemplate类实现. 缓存的原理..实现cache类接口,当哪个类需要缓存的时候,就直接将cache标签引入,并且制定我们的缓存类就可以了. 上代码: 1.引入spring-data-redis 1 <!-- redis服务 start--> 2 <dependency> 3 <groupId>org.springframework.data</g

mybatis 学习五 二级缓存不推荐使用

mybatis 二级缓存不推荐使用 一 mybatis的缓存使用. 大体就是首先根据你的sqlid,参数的信息自己算出一个key值,然后你查询的时候,会先把这个key值去缓存中找看有没有value,如果有,直接返回出来,就不查询db了.如果没有,那么查询db,然后将key,value保存到缓存中,以便下次使用. 1.1mybatis的一级缓存是基于sqlsession为生命周期的 当你这个session没有了,缓存就没有了,其次当你sql执行!isselect语句的时候,缓存也会被直接全部清理掉

MyBatis系列目录--5. MyBatis一级缓存和二级缓存(redis实现)

转载请注明出处哈:http://carlosfu.iteye.com/blog/2238662 0. 相关知识: 查询缓存:绝大数系统主要是读多写少. 缓存作用:减轻数据库压力,提供访问速度. 1. 一级缓存测试用例 (1) 默认开启,不需要有什么配置 (2) 示意图 (3) 测试代码 Java代码   package com.sohu.tv.cache; import org.apache.ibatis.session.SqlSession; import org.junit.After; i