mybatis二级缓存实现--protostuff序列化

来源:互联网 发布:软件项目经理面试 编辑:程序博客网 时间:2024/06/05 22:38

背景

  在使用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