使用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)
可以看出该接口实现了各种序列化方式,这些也可以在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>
来开启二级缓存