mybatis二级缓存--redis实现

背景

  在使用mybatis框架自带的二级缓存实现时有个问题就是: 部署多个实例会带来缓存不一致的情况,因为它是使用本地内存。于是有的选择不使用mybatis的二级缓存,干脆自己来写缓存和读缓存,一种普遍的做法就是先从redis中读取,没有就读库,然后再回写缓存供下次使用。这样会有两个问题, 第一 作为开发人员重点关注的应该是数据库,现在还要花费精力来关心缓存 ;第二 数据可能清除的不干净,比如有一条数据 A ,有单独存放他的一条缓存记录,也有存放了一个集合的,集合里面也包括了A记录,在更新A的时候须要清除A相关的数据,这样须要清除A的单条记录还要清除包括了A记录的集合,更可怕的是有时候我们并不知道A还在什么地方给缓存了。

解决方法

  第一种:类似于spring事务的方式(切面)来对待缓存,在调用读方法的时候执行读缓存的切面,如果有数据就直接返回,没有则进一步读库,这里不做介绍,spring4 及之后版本已经提供了相关实现(@Cache注解)。第二种:自己实现mybatis的Cache接口,作为二级缓存实现 ,这也是本文介绍的。

具体实现

直接上代码
MybatisRedisCache:

public class MybatisRedisCache implements Cache{

    private static Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
    /** The ReadWriteLock. */
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private static  Cluster jimClient;

    private String id;

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

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

    public void putObject(Object key, Object value) {
        if(value == null){
            return;
        }
        DataEntity data = new DataEntity();
        data.setObject(value);
        jimClient.hSet(id.getBytes(),key.toString().getBytes(),  SerializeUtil.serialize(data));
    }

    public Object getObject(Object key) {
        byte[] bytes = jimClient.hGet(id.getBytes(),key.toString().getBytes());
        if(bytes == null || bytes.length == 0){
            return null;
        }
        DataEntity data = SerializeUtil.deserialize(bytes,DataEntity.class);
        return data.getObject();
    }

    public Object removeObject(Object key) {
        return jimClient.hDel( id.getBytes(),key.toString().getBytes());
    }

    /**
     * 更新的时候会调用 
     */
    public void clear() {
        jimClient.del(id.getBytes());
    }

    public int getSize() {
        return  jimClient.hGetAll(id.getBytes()).size();
    }

    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    public static  void setJimClient(Cluster jimClient){
        MybatisRedisCache.jimClient = jimClient;
    }
}

MybatisRedisCache 实现了mybatis的Cache接口。mybatis在使用Cache的实现时,每个mapper都会有一个Cache实例。其中id就是mapper.xml文件中的namespace,在实例化时mybatis会自动传入。mybatis划分的粒度就是namespace级别的,就是说每个mapper都有自己的缓存空间,在更新数据(增删改)操作是会清空当前namespace的缓存(调用clear()方法),而不是所有数据的缓存。看网上有些实现 clear方法直接就是清空了所有数据,这样命中率还是很低的。namespace A的更新操作导致namespace B的数据被清空显然是不合理的。
这种实现本质就是每个mapper都有一个名为namespace的HashMap(在redis中以Map来存放数据)。jimClient 是京东对redis的包装实现,兼容redis。jimClient 由springIOC管理,因为别的地方也有使用,而MybatisRedisCache并不由springIOC管理,这样问题来了,一个不受IOC管理的对象如何注入受IOC容器管理的对象。这里把jimClient 定义为static的成员变量,然后通过静态注入的方式来注入,下面的RedisCacheTransfer类主要就是赋值操作。

SerializeUtil类

public class SerializeUtil {
 private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<>();

    private static Objenesis objenesis = new ObjenesisStd(true);

    private SerializeUtil() {
    }

    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> cls) {
        Schema<T> schema = (Schema<T>) cachedSchema.get(cls);
        if (schema == null) {
            schema = RuntimeSchema.createFrom(cls);
            if (schema != null) {
                cachedSchema.put(cls, schema);
            }
        }
        return schema;
    }

    /**
     * 序列化(对象 -> 字节数组)
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> cls = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(cls);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化(字节数组 -> 对象)
     */
    public static <T> T deserialize(byte[] data, Class<T> cls) {
        try {
            T message = (T) objenesis.newInstance(cls);
            Schema<T> schema = getSchema(cls);
            ProtostuffIOUtil.mergeFrom(data, message, schema);
            return message;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

  SerializeUtil 负责序列化,它基于protostuff 实现。protostuff包装了protobuf(google提供的序列化实现),而直接使用protobuf需要定义IDL文件,然后生成的bean类都好几千行,使用起来还是很不习惯的。protostuff可以序列化我们熟悉的bean对象。
  关于序列化的选择最直接的就是java内置序列化,但是它有很多问题,而现在的序列化手段还是很多的,所以尝试点新的东西,比如现在rpc框架基本上不会使用java内置序列化。protostuff序列化的时候不会保存当前对象的类信息,所以反序列化的时候须要传递class信息。而我们放在缓存中的数据可是任何类型的,因此不能直接序列化我们要存放的对象,getObject()方法可是根据key直接获取对象的。DataEntity 的结构很简单,现在只有一个Object的属性(后期可以根据需求添加相关附加属性),它就是为了包装我们要缓存的数据而生的。其实protostuff虽然不保存当然序列化对象的类信息,但是会保存内部属性的类信息,所以DataEntity 里面的object就可以存放数据了,反序列化时object可以正常的被反序列化出来真实的类型。

RedisCacheTransfer类

public class RedisCacheTransfer {
    @Autowired
    public void setJimClient(Cluster jimClient) {
        MybatisRedisCache.setJimClient(jimClient);
    }
}

  RedisCacheTransfer没啥好说的,为静态注入而生的。
DataEntity 类

public class DataEntity {

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }
}

  DataEntity 作为数据的载体而生,适应protostuff的序列化方式。
  实际使用时和mybatis的二级缓存使用一样,只需要将mapper.xml文件中的cache标签配置为MybatisRedisCache。有些不足的地方,还请指正,大家一起相互学习。整个实现算是一个大杂烩,把自己看到的好多东西揉在了一起。

参考资料:

https://github.com/mybatis/redis-cache
http://blog.csdn.net/xiadi934/article/details/50786293

<script type="text/javascript"> $(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('<ul/>').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('<li/>').text(i)); }; $numbering.fadeIn(1700); }); }); </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值