使用protostuff + redis做mybatis二级缓存

实操

		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
		<dependency>
			<groupId>io.protostuff</groupId>
			<artifactId>protostuff-core</artifactId>
			<version>1.5.9</version>
		</dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.5.9</version>
        </dependency>

由于使用的spring-boot-starter-data-redis,它是对redis客户端的一层封装,默认使用的lettuce客户端,这里使用jedis,所以先exclusion掉它

/**
 * Created by zkk on 2019/3/14
 * 增加protostuff序列化方式,生成的码流比jdk序列化小,速度更快
 * 解决devtool热加载在jdk序列化下类型转换报错的情况
 **/
public class ProtostuffSerializer implements RedisSerializer<Object> {

    private boolean isEmpty(byte[] data) {
        return (data == null || data.length == 0);
    }

    private final Schema<ProtoWrapper> schema;

    private final ProtoWrapper wrapper;

    private final LinkedBuffer buffer;

    public ProtostuffSerializer() {
        this.wrapper = new ProtoWrapper();
        this.schema = RuntimeSchema.getSchema(ProtoWrapper.class);
        this.buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
    }

    @Override
    public byte[] serialize(Object t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        wrapper.data = t;
        try {
            return ProtostuffIOUtil.toByteArray(wrapper, schema, buffer);
        } finally {
            buffer.clear();
        }
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if (isEmpty(bytes)) {
            return null;
        }

        ProtoWrapper newMessage = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(bytes, newMessage, schema);
        return newMessage.data;
    }

    // Protostuff 无法直接序列化集合类对象,需要包装类包一下
    private static class ProtoWrapper {
        public Object data;
    }
}

然后就是序列化的实现类,它实现了RedisSerializer接口
主要的方法就是byte[] serialize(T t)和 deserialize(byte[] bytes)
使用protostuff + redis做mybatis二级缓存
可以看出该接口实现了各种序列化方式,这些也可以在RedisTemplate里面使用,可以set它的序列化方式,比如常用的stringRedisTemplate就是使用的StringRedisSerializer序列化方式

/**
 * Created by zkk on 2019/2/1
 * mybatis的redis二级缓存
 * 需要用到缓存的就在mapper.xml里面加上
 * <cache type="com.xxx.xxx.config.MybatisRedisCache"/>
 **/
public class MybatisRedisCache implements Cache {
    private static Logger LOGGER = LogManager.getLogger(MybatisRedisCache.class);
    private final String id;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private static JedisConnectionFactory jedisConnectionFactory;

    /**
     * 这个地方需要静态注入,这里通过中间类 MybatisRedisCacheTransfer 实现的
     *
     * @param jedisConnectionFactory
     */
    public static void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
        MybatisRedisCache.jedisConnectionFactory = jedisConnectionFactory;
    }

    public MybatisRedisCache(final String id) {
        if (null == id || "".equals(id)) {
            throw new IllegalArgumentException("mybatis redis cache need an id.");
        }
        this.id = id;
        LOGGER.debug("mybatis redis cache id: {}", id);
    }


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

    /**
     * 存值
     *
     * @param key
     * @param value
     */
    @Override
    public void putObject(Object key, Object value) {
        if (null == key) {
            return;
        }
        LOGGER.debug("mybatis redis cache put. K={} value={}", key, value);
        RedisConnection redisConnection = null;
        try {
            redisConnection = jedisConnectionFactory.getConnection();
            RedisSerializer serializer = new ProtostuffSerializer();

            redisConnection.setEx(serializer.serialize(key), 500, serializer.serialize(value));

            // 将key保存到redis.list中
            redisConnection.lPush(serializer.serialize(id), serializer.serialize(key));
            redisConnection.expire(serializer.serialize(id), 500);
        } catch (Exception e) {
            LOGGER.error("mybatis redis cache put exception. K=" + key + " V=" + value + "", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
    }

    /**
     * 取值
     *
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        if (null == key) {
            return null;
        }
        LOGGER.debug("mybatis redis cache get. K={}", key);
        RedisConnection redisConnection = null;
        Object result = null;
        try {
            redisConnection = jedisConnectionFactory.getConnection();
            RedisSerializer serializer = new ProtostuffSerializer();
            result = serializer.deserialize(redisConnection.get(serializer.serialize(key)));
        } catch (Exception e) {
            LOGGER.error("mybatis redis cache get exception. K=" + key + " V=" + result + "", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return result;
    }

    /**
     * 删值
     *
     * @param key
     * @return
     */
    @Override
    public Object removeObject(Object key) {
        if (null == key) {
            return null;
        }
        LOGGER.debug("mybatis redis cache remove. K={}", key);
        RedisConnection redisConnection = null;
        Object result = null;
        try {
            redisConnection = jedisConnectionFactory.getConnection();
            RedisSerializer serializer = new ProtostuffSerializer();
            // 讲key设置为立即过期
            result = redisConnection.expireAt(serializer.serialize(key), 0);

            // 将key从redis.list中删除
            redisConnection.lRem(serializer.serialize(id), 0, serializer.serialize(key));
        } catch (Exception e) {
            LOGGER.error("mybatis redis cache remove exception. " + key + " V=" + result + "", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return result;
    }

    /**
     * 清空缓存
     * flushCache="true" 的时候会调用这个地方
     */
    @Override
    public void clear() {
        LOGGER.debug("mybatis redis cache clear. ");
        RedisConnection redisConnection = null;
        try {
            redisConnection = jedisConnectionFactory.getConnection();
            RedisSerializer serializer = new ProtostuffSerializer();
            /**
             * 千万不要直接 redisConnection.flushDb(),会把整个redis的东西都清除掉,我不相信你的redis里没有其他东西
             * 获取redis.list中的保存的key值,遍历删除
             */
            Long length = redisConnection.lLen(serializer.serialize(id));
            if (0 == length) {
                return;
            }
            List<byte[]> keyList = redisConnection.lRange(serializer.serialize(id), 0, length - 1);
            for (byte[] key : keyList) {
                redisConnection.expireAt(key, 0);
            }
            redisConnection.expireAt(serializer.serialize(id), 0);
            keyList.clear();
        } catch (Exception e) {
            LOGGER.error("mybatis redis cache clear exception. ", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
    }

    @Override
    public int getSize() {
        int result = 0;
        try {
            RedisConnection redisConnection = jedisConnectionFactory.getConnection();
            result = Math.toIntExact(redisConnection.dbSize());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

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

这些是在网上找的demo,修改了些参数,现在能很好的运行的代码

需要注意的是原来使用的RedisSerializer serializer = new JdkSerializationRedisSerializer();
就是jdk的序列化方式,发现在spring-boot-devtools热加载工具开启的情况下经常会发生java.lang.ClassCastException:异常,就是两个相同的类不能cast,也是很神奇,现在换为Protostuff序列化就没有这种情况了,并且生成的码流比jdk的小,速度快。

然后在mybatis-config.xml配置上加上一句

<setting name="cacheEnabled" value="true"/>

最后在需要的mapper.xml加上

<mapper namespace="com.xxx.mapper.xxx">
 <cache type="com.config.MybatisRedisCache"/>
 </mapper>

来开启二级缓存