在spring boot中集成redis
How to build redis in spring boot project.
1. 怎么在spring boot中集成redis?
一个响应快速的系统,必然要在请求资源服务接口中避免所有能避免的DB操作,这时候redis是一个相当不错的选择。它稳定,高效,简便,还支持分布式扩展。
(1)先在application.yml中配置本地单机redis,这里有一个坑,就是在Spring Boot 1.2及更高的版本中,spring.redis.pool.max-active等属性已经过期,只能使用spring.redis.jedis.pool.max-active来代替。关于spring redis的默认配置,可以参考org.springframework.boot.autoconfigure.data.redisRedisProperties类。

(2)然后我们指定下redisTemplate类的序列化方式为Jackson2JsonRedisSerializer,如果不指定默认使用JdkSerializationRedisSerializer。这里说下,spring data提供了2种json序列化器,GenericJackson2JsonRedisSerializer和Jackson2JsonRedisSerializer,它们的序列化方式一样(调用的代码一样),即在序列化时将java类型和javaBean的json格式一起写入byte数组。然后在反序列化时,根据java类型将字节数组反序列化为对应类型的javaBean。

不同之处在于:GenericJackson2JsonRedisSerializer在反序列化时多了个参数java类型,可以根据传入的java类型反序列化。

我们配置下redis的序列化方式,然后测试下Jackson2JsonRedisSerializer能序列化的java类型。
/**
* 描述: Redis配置
*
* @author linfengda
* @create 2018-09-10 17:00
*/
@SpringBootConfiguration
public class RedisConfig {
/**
* 如果不配置这个bean,会默认初始化一个使用JdkSerializationRedisSerializer的redisTemplate实例。
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化和反序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(3)Jackson2JsonRedisSerializer对java类型的兼容性测试,测试它对每种java类型的序列化和反序列化是否会有问题。
/**
* 描述: 序列化的类型测试
*
* @author linfengda
* @create 2018-09-12 15:54
*/
@Data
public class MySon extends YourPapa {
private int f1;
private long f2;
private float f3;
private double f4;
private char f5;
private boolean f6;
private String f7;
private Timestamp f8;
private int[] f9;
private long[] f10;
private float[] f11;
private double[] f12;
private char[] f13;
private boolean[] f14;
private String[] f15;
private Pig pig;
private List<Pig> childPigs;
private Map<String, Pig> kindPigs;
}
/**
* 描述: 这是一只猪
*
* @author linfengda
* @create 2018-09-12 16:08
*/
@Data
public class Pig {
private long id;
private String code;
private String master;
private double weight;
private int growDays;
}
(4)使用Chapter1ApplicationTestslei类测试下redisTemplate的使用,这里我们需要先把/main/resources目录下的配置文件复制到/test/resources,否则会报找不到配置文件错误。然后我们使用上面的java类进行测试。

(5)Redis Desktop Manager查看信息如下,可以看到头部带有java类型信息。支持度比Protobuff好,可以看到我们使用了几乎所有常用的数据类型,包括: 列表,哈希表,继承,自定义类型, 都能序列化和反序列化成功,并且没有信息丢失。

(6)接下来我们测试下Jackson2JsonRedisSerializer序列化器对redis数据类型: List, Set, Hash的支持。
List类型操作
redis list是使用双向链表实现的,对redis list的操作更像对队列的操作,除了LINDEX,LSET,LINSERT命令外,其它命令的时间复杂度都为O(1)。
private void ListSerializeTest() {
Pig peggy = new Pig();
peggy.setId(1);
peggy.setCode("peggy");
peggy.setMaster("Jack");
peggy.setWeight(100);
peggy.setGrowDays(100);
Pig george = new Pig();
george.setId(2);
george.setCode("george");
george.setMaster("Jack");
george.setWeight(150);
george.setGrowDays(150);
Pig tom = new Pig();
tom.setId(2);
tom.setCode("tom");
tom.setMaster("Jack");
tom.setWeight(120);
tom.setGrowDays(120);
Pig wilson = new Pig();
wilson.setId(2);
wilson.setCode("wilson");
wilson.setMaster("Jack");
wilson.setWeight(110);
wilson.setGrowDays(110);
List<Pig> pigList = new ArrayList<>();
pigList.add(tom);
pigList.add(wilson);
log.info("===================================redis list序列化");
ListOperations<String, Object> listOperations = simpleRedisTemplate.opsForList();
listOperations.rightPushAll("pigList", peggy, george);
listOperations.rightPushAll("pigList", pigList.toArray());
// 由于org.springframework.data.redis.core.ListOperations的2个重载方法破坏了里氏替换原则,
// 对rightPushAll(K key, Collection<V> values)方法的调用会变成对rightPushAll(K key, V... values)方法的调用
//listOperations.rightPushAll("pigList", pigList);
log.info("===================================redis list反序列化");
// 左边元素出队列(出队列后该值在列表中将不存在)
log.info(listOperations.leftPop("pigList").toString());
log.info(listOperations.leftPop("pigList").toString());
// 返回列表中指定位置的元素(不会移除列表中元素)
List pigs = listOperations.range("pigList", 0, -1);
for (Object pig : pigs) {
log.info(pig.toString());
}
}
输出的反序列化信息如下:

Set类型操作
redis set是使用整数集合或hashtable实现的,对于元素都是整形的集合,使用整数集合编码,对于字符串的集合。使用hashtable编码, 除了SMEMBERS,SUNION/SUNIONSTORE,SINTER/SINTERSTORE,SDIFF/SDIFFSTORE命令外,其它命令的时间复杂度都为O(1)。
private void SetSerializeTest() {
Pig peggy = new Pig();
peggy.setId(1);
peggy.setCode("peggy");
peggy.setMaster("Jack");
peggy.setWeight(100);
peggy.setGrowDays(100);
Pig george = new Pig();
george.setId(2);
george.setCode("george");
george.setMaster("Jack");
george.setWeight(150);
george.setGrowDays(150);
Pig tom = new Pig();
tom.setId(3);
tom.setCode("tom");
tom.setMaster("Jack");
tom.setWeight(120);
tom.setGrowDays(120);
Pig wilson = new Pig();
wilson.setId(4);
wilson.setCode("wilson");
wilson.setMaster("Jack");
wilson.setWeight(110);
wilson.setGrowDays(110);
log.info("===================================redis list序列化");
SetOperations<String, Object> setOperations = simpleRedisTemplate.opsForSet();
// 向集合中添加元素
setOperations.add("pigFamily", peggy, george, tom, wilson);
// 移除集合中一个或多个元素
setOperations.remove("pigFamily", wilson);
// 判断元素peggy是否是集的成员
log.info("peggy is one of pigFamily: " + setOperations.isMember("pigFamily", peggy));
log.info("===================================redis list反序列化");
// 返回集合中的所有元素
Set pigs = setOperations.members("pigFamily");
for (Object pig : pigs) {
log.info(pig.toString());
}
}
输出的反序列化信息如下:

Hash类型操作
redis hash是使用hashtable实现的,除了HGETALL,HKEYS/HVALS命令外,其它命令的时间复杂度都为O(1)。
private void HashSerializeTest() {
Pig peggy = new Pig();
peggy.setId(1);
peggy.setCode("peggy");
peggy.setMaster("Jack");
peggy.setWeight(100);
peggy.setGrowDays(100);
Pig george = new Pig();
george.setId(2);
george.setCode("george");
george.setMaster("Jack");
george.setWeight(150);
george.setGrowDays(150);
Pig tom = new Pig();
tom.setId(3);
tom.setCode("tom");
tom.setMaster("Jack");
tom.setWeight(120);
tom.setGrowDays(120);
Pig wilson = new Pig();
wilson.setId(4);
wilson.setCode("wilson");
wilson.setMaster("Jack");
wilson.setWeight(110);
wilson.setGrowDays(110);
log.info("===================================redis list序列化");
HashOperations<String, String, Object> hashOperations = simpleRedisTemplate.opsForHash();
// 向哈希表中添加元素
hashOperations.put("pigFamily", "peggy", peggy);
hashOperations.put("pigFamily", "george", george);
hashOperations.put("pigFamily", "tom", tom);
hashOperations.put("pigFamily", "wilson", wilson);
// 移除哈希表中一个或多个元素
hashOperations.delete("pigFamily", "wilson");
log.info("===================================redis list反序列化");
// 获取哈希表中的元素
Pig peggyPig = (Pig) hashOperations.get("pigFamily", "peggy");
log.info(peggyPig.toString());
// 判断哈希表中是否存在元素peggy
log.info("peggy is one of pigFamily: " + hashOperations.hasKey("pigFamily", "peggy"));
}
输出的反序列化信息如下:

2. 尝试更加高效的序列化方式:protostuff
(6)接下来我们使用Protobuff实现序列化,ProtocolBuffer在谷歌被广泛用于各种结构化信息存储和交换。这里有个问题,就是ProtocolBuffer序列化时没有保存类型信息,因此在反序列化时需要传入java类型,这与org.springframework.data.redis.serializer.RedisSerializer.deserialize(@Nullable byte[] bytes)接口不兼容。该接口被设计为只处理字节数组,无法传入类型信息。
要解决这个问题,我们需要先搞清楚springframework.data.redis的api操作层的代码设计:

从上面的类结构设计图中可以看出,springframework.data.redis的设计只考虑了代码职责单一性,完全没有考虑代码的可用性和扩展性,简直就是一大败笔。RedisTemplate<K, V>类与它使用的各个操作类之间是聚合关系,DefaultValueOperations,DefaultListOperations,DefaultSetOperations,DefaultHashOperations都是RedisTemplate<K, V>类的全局变量。而且,反序列化代码部分是直接写死在AbstractOperations父类中的,它与各个操作类之间是继承关系,这部分代码按道理是要解耦出来,让开发人员可以扩展。综上所述,如果我们要在springframework.data.redis中完美集成ProtocolBuffer这种反序列化时需要类型信息的序列化方式,需要重构整个springframework.data.redis的api操作层的代码,这简直太恐怖了。这就是老程序员们为什么一直强调代码设计要可扩展的原因,总有一些中二的程序员,他们的工作导致了别人需要花费数倍的工作来完成后续功能,这样的程序员我们称之为负产出程序员。
所以我们退而求其次,在ProtoStuffSerializer类的deserialize(@Nullable byte[] bytes)方法中直接返回字节数组,然后在SimpleRedisTemplate4PS模板中处理。

这样处理其实有一个隐患,就是反序列化数组的时候,需要额外的遍历操作,这直接将反序列化的时间复杂度由原来的O(n)变成O(2n)。

同时对于String类型,我们需要在ProtoStuffUtil反序列化方式中快捷处理,不然走ProtoStuff原来的反序列化方式会出错。

3. protostuff的坑
(1)不能反序列化java.sql.TimeStamp,如Myson.class的f8字段:

反序列化之后变成了格林威治时间+x:

(2)反序列化时按字段顺序赋值而非名称赋值,protostuff序列化一个对象时是按字段顺序把值序列化到字节码中,反序列化时也是按字段顺序赋值。如果序列化后修改了字段名称,反序列化时会将旧字段的值赋值到新字段。如序列化一个Myson.class的实例,然后修改MySecondson.class的字段名f7为f77,反序列化之后f77被赋值为f7的值:



(3)序列化后不能修改对应顺序字段的类型,如MySecondson.class中,将f7字段类型由String修改为Object,会出现反序列失败报错。


为了避免以上问题,在使用protostuff序列化时,如果需要修改已有的实体,添加字段放到最后去就可以了。
4. 模块化编程和解耦
(1)我们在application.yml配置文件中,使用配置项spring.redis.serializer来配置序列化方式,例如:spring.redis.serializer:protoStuff表示使用protoStuff序列化器。
redis:
serializer: jackson
host: 127.0.0.1
port: 6379
database: 0
timeout: 5000
jedis:
pool:
max-active: 200
max-idle: 8
max-wait: -1
这样就可以通过参数直接指定redis序列化方式。
package com.linfengda.sb.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.linfengda.sb.support.middleware.redis.SimpleRedisTemplate;
import com.linfengda.sb.support.middleware.redis.SimpleRedisTemplate4JS;
import com.linfengda.sb.support.middleware.redis.SimpleRedisTemplate4PS;
import com.linfengda.sb.support.middleware.redis.serializer.ProtoStuffSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.Assert;
/**
* 描述: Redis配置
*
* @author linfengda
* @create 2018-09-10 17:00
*/
@SpringBootConfiguration
public class RedisConfig {
@Value("${spring.redis.serializer}")
private String serializer;
/**
* 序列化类型
*/
public enum Serializer {
protoStuff, jackson;
public static Serializer getType(String serializer) {
for (Serializer s : values()) {
if (s.name().equals(serializer)) {
return s;
}
}
return jackson;
}
}
/**
* 注入包含redis常见操作的模板
*
* @param connectionFactory
* @return
*/
@Bean
public SimpleRedisTemplate simpleRedisTemplate(RedisConnectionFactory connectionFactory) {
Serializer serializer = Serializer.getType(this.serializer);
Assert.notNull(serializer, "序列化方式不能为空!");
SimpleRedisTemplate simpleRedisTemplate = getRedisTemplate(serializer);
RedisSerializer redisSerializer = getRedisSerializer(serializer);
simpleRedisTemplate.setConnectionFactory(connectionFactory);
// 配置序列化和反序列化方式
simpleRedisTemplate.setKeySerializer(new StringRedisSerializer());
simpleRedisTemplate.setHashKeySerializer(new StringRedisSerializer());
simpleRedisTemplate.setValueSerializer(redisSerializer);
simpleRedisTemplate.setHashValueSerializer(redisSerializer);
simpleRedisTemplate.afterPropertiesSet();
return simpleRedisTemplate;
}
private SimpleRedisTemplate getRedisTemplate(Serializer serializer) {
switch (serializer) {
case protoStuff:
return new SimpleRedisTemplate4PS();
case jackson:
return new SimpleRedisTemplate4JS();
default:
return null;
}
}
private RedisSerializer getRedisSerializer(Serializer serializer) {
switch (serializer) {
case protoStuff:
ProtoStuffSerializer protoStuffSerializer = new ProtoStuffSerializer();
return protoStuffSerializer;
case jackson:
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
default:
return null;
}
}
}
定义SimpleRedisTemplate类统一的数据结构的操作行为,使模板与序列化方式解耦。
package com.linfengda.sb.support.middleware.redis;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* 描述: 统一的数据结构的操作行为,使模板与序列化方式解耦。
*
* @author linfengda
* @create 2018-10-03 20:04
*/
public abstract class SimpleRedisTemplate extends RedisTemplate<String, Object> {
public abstract void setObject(String key, Object value);
public abstract <T> T getObject(String key, Class<T> clazz);
public abstract Boolean deleteObject(String key);
public abstract void listAdd(String key, Object... values);
public abstract void listAddAll(String key, Collection values);
public abstract <T> List<T> listGet(String key, Class<T> clazz);
public abstract void setAdd(String key, Object... values);
public abstract void setAddAll(String key, Collection values);
public abstract <T> Set<T> setGet(String key, Class<T> clazz);
public abstract Long setDelete(String key, Object... values);
public abstract void mapPut(String key, String hashKey, Object value);
public abstract <T> T mapGet(String key, String hashKey, Class<T> clazz);
public abstract Long mapDelete(String key, String... hashKeys);
}
jackson实现类
package com.linfengda.sb.support.middleware.redis;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* 描述: 当使用Jackson2JsonRedisSerializer时,需要使用这个模板。
*
* @author linfengda
* @create 2018-09-12 13:40
*/
public class SimpleRedisTemplate4JS extends SimpleRedisTemplate {
@Override
public void setObject(String key, Object value) {
super.opsForValue().set(key, value);
}
@Override
public <T> T getObject(String key, Class<T> clazz) {
T object = (T) super.opsForValue().get(key);
return object;
}
@Override
public Boolean deleteObject(String key) {
return super.delete(key);
}
@Override
public void listAdd(String key, Object... values) {
super.opsForList().rightPushAll(key, values);
}
@Override
public void listAddAll(String key, Collection values) {
for (Object value : values) {
super.opsForList().rightPushAll(key, value);
}
}
@Override
public <T> List<T> listGet(String key, Class<T> clazz) {
List list = super.opsForList().range(key, 0, -1);
return null == list ? null : (List<T>) list;
}
@Override
public void setAdd(String key, Object... values) {
super.opsForSet().add(key, values);
}
@Override
public void setAddAll(String key, Collection values) {
for (Object value : values) {
super.opsForSet().add(key, value);
}
}
@Override
public <T> Set<T> setGet(String key, Class<T> clazz) {
Set set = super.opsForSet().members(key);
return null == set ? null : (Set<T>) set;
}
@Override
public Long setDelete(String key, Object... values) {
return super.opsForSet().remove(key, values);
}
@Override
public void mapPut(String key, String hashKey, Object value) {
super.opsForHash().put(key, hashKey, value);
}
@Override
public <T> T mapGet(String key, String hashKey, Class<T> clazz) {
return (T) super.opsForHash().get(key, hashKey);
}
@Override
public Long mapDelete(String key, String... hashKeys) {
return super.opsForHash().delete(key, hashKeys);
}
}
protoStuff实现类
package com.linfengda.sb.support.middleware.redis;
import java.util.*;
/**
* 描述: 当使用ProtoStuffSerializer时,需要使用这个模板。
*
* @author linfengda
* @create 2018-10-03 20:02
*/
public class SimpleRedisTemplate4PS extends SimpleRedisTemplate {
@Override
public void setObject(String key, Object value) {
super.opsForValue().set(key, value);
}
@Override
public <T> T getObject(String key, Class<T> clazz) {
byte[] bytes = (byte[]) super.opsForValue().get(key);
if (bytes == null) {
return null;
} else {
return ProtoStuffUtil.deserialize(bytes, clazz);
}
}
@Override
public Boolean deleteObject(String key) {
return super.delete(key);
}
@Override
public void listAdd(String key, Object... values) {
super.opsForList().rightPushAll(key, values);
}
@Override
public void listAddAll(String key, Collection values) {
for (Object value : values) {
super.opsForList().rightPushAll(key, value);
}
}
@Override
public <T> List<T> listGet(String key, Class<T> clazz) {
List list = super.opsForList().range(key, 0, -1);
if (list == null) {
return null;
} else {
for (int i = 0; i < list.size(); i++) {
byte[] bytes = (byte[]) list.get(i);
if (bytes != null) {
list.set(i, ProtoStuffUtil.deserialize(bytes, clazz));
}
}
return (List<T>) list;
}
}
@Override
public void setAdd(String key, Object... values) {
super.opsForSet().add(key, values);
}
@Override
public void setAddAll(String key, Collection values) {
for (Object value : values) {
super.opsForSet().add(key, value);
}
}
@Override
public <T> Set<T> setGet(String key, Class<T> clazz) {
Set set = super.opsForSet().members(key);
if (set == null) {
return null;
} else {
Set set2 = new HashSet(set.size());
Iterator it = set.iterator();
while (it.hasNext()) {
byte[] bytes = (byte[]) it.next();
if (bytes != null) {
set2.add(ProtoStuffUtil.deserialize(bytes, clazz));
}
}
return (Set<T>) set2;
}
}
@Override
public Long setDelete(String key, Object... values) {
return super.opsForSet().remove(key, values);
}
@Override
public void mapPut(String key, String hashKey, Object value) {
super.opsForHash().put(key, hashKey, value);
}
@Override
public <T> T mapGet(String key, String hashKey, Class<T> clazz) {
byte[] bytes = (byte[]) super.opsForHash().get(key, hashKey);
if (bytes == null) {
return null;
} else {
return ProtoStuffUtil.deserialize(bytes, clazz);
}
}
@Override
public Long mapDelete(String key, String... hashKeys) {
return super.opsForHash().delete(key, hashKeys);
}
}
(2)然后我们最终的目的是实现通过参数配置redis序列化器,且序列化方式对SimpleRedisTemplate方法的调用者透明。各模块间的关系设计如下:配置序列化方式--->实例化SimpleRedisTemplate的具体实例--->注入容器使用。代码设计到这里,SimpleRedisTemplate类已经实现了对redis各种类型的操作,同时与序列化方式解耦。