April 21, 2018

在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各种类型的操作,同时与序列化方式解耦。