1 Redis简介

 Redis 是完全开源免费的、遵守BSD协议的高性能数据库。Redis支持String,list,set,zset,hash等数据结构的key-value存储。它支持数据的持久化,支持master-slave模式的数据备份,支持事务。
Redis 优势:
(1)性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
(2)丰富的数据类型 – Redis支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
(3)原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
(4)丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

2 Redis的Java客户端配置

Redis的java客户端有非常多,下面是Redis官网列出的Java客户端:

名称

介绍

aredis

基于 Java 7 NIO Channel API的客户端

JDBC-Redis

mavcunha

Jedis

xetorthio

JRedis

SunOf27

lettuce

线程安全的支持同步、异步、响应式的高级Java客户端 ,支持集群、哨兵、Pipelining和codecs

mod-redis

Vert.x (事件驱动框架)官方的 redis.io 消息总线模块

redis-protocol

兼容2.6+版本,高性能的 Java, Java w/Netty & Scala (finagle) client 客户端

RedisClient

图形化Redis客户端

Redisson

包括分布式锁的分布式、可伸缩Java数据结构

RJC

  最受欢迎的有Jedis、Lettuce、RedisTemplate,Lettuce基于Netty框架开发,支持同步、异步和响应式模式,多个线程可以共享一个连接实例,而不必担心多线程并发问题;Spring提供的RedisTemplate在Lettuce基础上做了进一步封装,使用起来更加方便。我们试用一下Jedis、RedisTemplate。先上基础设施:Dao接口和工具类:
(1)Dao接口

public interface ICacheDao {
    boolean save(UserEntity userEntity);

    UserEntity get(int id);

    boolean delete(int id);
}

(2)自定义Map和Entity互转工具类。这套简单互转工具我们在Jedis中试用,在RedisTemplate使用时专业的序列化/反序列化工具。

public class EntityUtil {

    /**
     * 实体转Map
     * @param o
     * @return
     */
    public static Map<String, String> entityToMap(Object o){
        if (o == null){
            return new HashMap<>(0);
        }
        Class clazz = o.getClass();
        Field[] fields = clazz.getDeclaredFields();
        Map<String,String> map = new HashMap<>();

        for (Field field:fields){
            field.setAccessible(true);
            try {
                Object value = field.get(o);
                if (value == null){
                    continue;
                }
                map.put(field.getName(), value.toString());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return map;
    }


    /**
     * Map转实体
     * @param map
     * @param <T>
     * @return
     */
    public static <T> T mapToEntity(Map<String, String> map, Class<T> clazz){
        if (map == null || map.isEmpty() || clazz == null){
            return null;
        }

        T instance = null;
        try {
            instance = clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }

        if (instance == null){
            return null;
        }

        for (Map.Entry<String,String> entry:map.entrySet()){
            try {
                Field field = clazz.getDeclaredField(entry.getKey());
                field.setAccessible(true);
                field.set(instance, entry.getValue());

            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return instance;
    }
}
2.1 Jedis方式

(1)application.properties配置

spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=4
spring.redis.host=localhost

(2)创建JedisPool

@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class JedisConfig {
    private int maxActive = 8;
    private int maxIdle = 8;
    private String host = "localhost";
    private int maxWaite = -1;

    private int port = 6379;
    private int timeOut = 30*1000;
    
    @Bean
    public JedisPool getJedisPool(){
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxActive);
        config.setMaxIdle(maxIdle);
        config.setMaxWaitMillis(maxWaite);
        config.setTestOnBorrow(true);
        config.setTestOnReturn(true);

        return new JedisPool(config, host, port);
    }

(3)DAO实现类

@Component
public class JedisCacheDao implements ICacheDao {
    @Autowired
    private JedisPool jedisPool;

    @Override
    public boolean save(UserEntity userEntity){
        Jedis jedis = jedisPool.getResource();
        String result = jedis.hmset(userEntity.getId() + "", EntityUtil.entityToMap(userEntity));

        return "OK".equals(result);
    }

    @Override
    public UserEntity get(int id){
        Jedis jedis = jedisPool.getResource();

        return EntityUtil.mapToEntity(jedis.hgetAll(id + ""), UserEntity.class);
    }


    @Override
    public boolean delete(int id){
        Jedis jedis = jedisPool.getResource();

        return jedis.del(id + "")>0;
    }
}

(3)测试类

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class CacheDaoTest {
    @Autowired
    @Qualifier("jedisCacheDao")
    private ICacheDao cacheDao;

    /**
     * Method: save(UserEntity userEntity)
     */
    @Test
    public void testSave() throws Exception {

        UserEntity userEntity = new UserEntity();
        userEntity.setId("1");
        userEntity.setAddress("localhost");
        userEntity.setName("him");

        boolean successBool = cacheDao.save(userEntity);
        System.out.println("save:" + successBool);
    }

    /**
     * Method: get(int id)
     */
    @Test
    public void testGet() throws Exception {
        UserEntity entity = cacheDao.get(1);
        if (entity == null) {
            System.out.println("没有找到实体");
        } else {
            System.out.println(entity.getAddress());
        }
    }

    @Test
    public void testDelete() throws Exception {
        boolean successBool = cacheDao.delete(1);
        System.out.println("delete:" + successBool);
    }
}
2.2 RedisTemplate方式

(1)application.properties配置

spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=4
spring.redis.host=localhost

(2)配置RedisTemplate

@Configuration
public class RedisTemplateConfig {

    /**
     * application.properties配置完成,Spring会配置RedisConnectionFactory这个Bean
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

(3)简单使用

@Component
public class RedisTemplateCacheDao implements ICacheDao {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public boolean save(UserEntity userEntity) {
        ValueOperations<String,Object> kv = redisTemplate.opsForValue();
        kv.set("" + userEntity.getId(), userEntity, 20, TimeUnit.SECONDS); //设置过期时间并保存

        return true;
    }

    @Override
    public UserEntity get(int id) {
        ValueOperations<String,Object> kv = redisTemplate.opsForValue();
        Object value = kv.get(id + "");
        if (value == null){
            return null;
        }
        return (UserEntity)value;
    }

    @Override
    public boolean delete(int id) {

        return redisTemplate.delete("" + id);
    }
}

(4)测试类

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisTemplateCacheDaoTest {
    @Autowired
    @Qualifier("redisTemplateCacheDao")
    private ICacheDao cacheDao;

    @Before
    public void before() throws Exception {
    }

    @After
    public void after() throws Exception {
    }

    /**
     * Method: save(UserEntity userEntity)
     */
    @Test
    public void testSave() throws Exception {

        UserEntity userEntity = new UserEntity();
        userEntity.setId("1");
        userEntity.setAddress("localhost");
        userEntity.setName("him");
        userEntity.setBirthDay(new Date());

        boolean successBool = cacheDao.save(userEntity);
        System.out.println("save:" + successBool);
    }

    /**
     * Method: get(int id)
     */
    @Test
    public void testGet() throws Exception {
        UserEntity entity = cacheDao.get(1);
        if (entity == null) {
            System.out.println("没有找到实体");
        } else {
            System.out.println(entity.getAddress());
            System.out.println(entity.getBirthDay());
        }
    }

    /**
     * Method: delete(int id)
     */
    @Test
    public void testDelete() throws Exception {
        boolean successBool = cacheDao.delete(1);
        System.out.println("delete:" + successBool);
    }

3 Redis的常用操作及适用场景

  我们分别描述Jedis和RedisTemplate的API。

3.1 Jedis的常用API
@Component
public class JedisOperation {
    @Autowired
    private JedisPool jedisPool;


    public void add(){
        Jedis jedis = jedisPool.getResource();
        /*****String*****/
        jedis.set("A", "StringValue");

        jedis.set("B","StringValueB", "XX", "PX", 20*1000);
        jedis.expire("Key", 1000);  //设置过期时间的两种方式,创建时设置、创建完成后设置

        /******List********/
        jedis.lpush("ListB", "ValueA", "ValueB", "ValueN");   //从左边加入2个元素
        jedis.lset("ListA", 0, "ListAValue");

        /******Set*********/
        jedis.sadd("ListB", "ValueA", "ValueA", "ValueN");

        /*******ZSet*********/
        jedis.zadd("ZSetA", 1, "A");
        jedis.zadd("ZSetA", 2, "B");

        /******Hash******/
        jedis.hmset("Map", new HashMap<String,String>(){{
            put("MapA", "B");
            put("MapB", "3");
        }});

    }


    public void modify(){
        Jedis jedis = jedisPool.getResource();
        /****String*****/
        jedis.append("A", "aotherString");    //追加
        jedis.set("A", "modified");   //修改

        /*****List*******/
        jedis.lset("ListA", 0, "sss");  //修改0这个位置的元素

        /****Set******/
        //Set不支持修改,只能删除

        /*****zSet*******/
        //ZSet不支持修改值,可以修改score
        jedis.zincrby("ZSetA", 100,"A");

        /********Hash*******/
        //Hash不支持字符串修改,支持数值加减
        jedis.hincrBy("Map", "MapB", 12); //MapB的值+12
    }


    public void get(){
        Jedis jedis = jedisPool.getResource();
        /****String*****/
        jedis.get("StringA");
        /*****List*******/
        jedis.lpop("ListKEY");  //从左边取出第一个
        jedis.rpop("ListKey");  //从右边取出第一个
        jedis.blpop(10, "ListKEY","ListKEYB");  //有时间限制的阻塞获取
        /******Set*******/
        jedis.smembers("SetKey");
        /******ZSet********/
        jedis.zrange("Key", 10, 20);  //取出10-20号元素
        /*******Map********/
        jedis.hgetAll("Key");  //取出Map
        jedis.hmget("key","Name","age"); //取出部分属性
    }


    public void delete(){
        Jedis jedis = jedisPool.getResource();

        /*****String*******/
        jedis.del("Key");  //删除键值对,所有类型都适用
        /*****List******/
        jedis.lrem("Key", 2, "value");  //删除2个存在的值value
        /*****Set******/
        jedis.spop("Key");    //随机删除集合中的一个元素
        /*****ZSet*******/
        jedis.zrem("key", "valueA", "ValueB");  //从集合ZSet中删除2个value;
        /******Map*********/
        jedis.zrem("key", "valueA","ValueB");  //从Map中删除2个value;
    }
}
3.2 RedisTemplate常用API
@Component
public class RedisTemplateOperation {


    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 5种数据类型的操作及适用场景
     */
    public void unboundHandle(){
        //【String】
        ValueOperations<String, Object> operations = redisTemplate.opsForValue();
        operations.set("string_key", "value");    //增
        operations.set("string_key", "value1");   //改
        Object object = operations.get("string_key");    //查

        //【List】,更像一个队列
        ListOperations<String,Object> listOperations = redisTemplate.opsForList();
        long longValue = listOperations.leftPush("list_key", "value1");    //增
        longValue = listOperations.leftPushAll("list_key", "value2", "value3", "value4", "value5");    //增

        listOperations.set("list_key", 0, "value0");    //改
        listOperations.trim("list_key", 1,4);    //删除start-end范围之外的数据
        object = listOperations.leftPop("list_key");    //从左侧取
        object = listOperations.rightPop("list_key", 1000, TimeUnit.MILLISECONDS);    //从右侧取,有超时时间
        object = listOperations.rightPopAndLeftPush("list_key", "list_key1");  //从目标右边取出并存入目的地左边
        object = listOperations.index("list_key", 0);    //取出0号元素
        longValue = listOperations.remove("list_key", 1, "value2");    //删除1个value2,没有则返回0


        //【Set】,更多用于集合的交并补操作
        SetOperations<String,Object> setOperations = redisTemplate.opsForSet();
        longValue = setOperations.add("set_key", "value1", "value2", "value3");   //增
        longValue = setOperations.add("set_key_1", "value1", "value2", "value3", "value4");   //增

        Set set = setOperations.members("set_key_1");  //查

        longValue = setOperations.size("set_key");  //元素个数
        boolean bool = setOperations.move("set_key", "value2", "set_key_new");    //移动元素
        Set<Object> objects = setOperations.intersect("set_key", "set_key_1");    //查询交集
        objects = setOperations.union("set_key", "set_key_1");    //查询并集
        objects = setOperations.difference("set_key", "set_key_1");  //查询补集(注:没有达到效果)

        longValue = setOperations.intersectAndStore("set_key", "set_key_1", "set_key_new");    //存储交集
        longValue = setOperations.unionAndStore("set_key", "set_key_1", "set_key_new");    //存储并集
        longValue = setOperations.differenceAndStore("set_key", "set_key_1","set_key_new");  //存储补集


        List<Object> list = setOperations.pop("set_key", 2);    //取出2个元素

        //【zSet】,更多用于集合内数据查询
        ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
        bool = zSetOperations.add("zset_key", "value0", 12);    //增
        bool = zSetOperations.add("zset_key", "value1", 19);    //增
        bool = zSetOperations.add("zset_key_1", "c", 12);    //增
        bool = zSetOperations.add("zset_key_1", "value1", 19);    //增
        longValue = zSetOperations.count("zset_key", 10,100);    //计算10<=score<=100的值个数
        Double doub = zSetOperations.incrementScore("zset_key", "value0", 8);   //value0的score += 8
        longValue = zSetOperations.intersectAndStore("zset_key", "zset_key_1", "zset_key_new");    //存储交集且score相加
        objects = zSetOperations.range("zset_key", 1,100);    //取1<=id<=100的值

        RedisZSetCommands.Range range = new RedisZSetCommands.Range();
        range.gte("a");    //大于等于10,gt(int min)大于
        range.lte("z");    //小于等于100,lt(int max)小于
        RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();
        limit.count(20);    //limit=20
        //类似关系型数据库 select * from zset_key where id<100 and id>0 limit 20;
        objects = zSetOperations.rangeByLex("zset_key", range, limit);
        //类似关系型数据库 select * from zset_key where score<100 and score>10 and id>5 limit 20;
        objects = zSetOperations.rangeByScore("zset_key", 10, 100, 1, 20);
        //类似关系型数据库 select id from zset_key where value='value0';
        longValue = zSetOperations.rank("zset_key", "value0");
        //类似关系型数据库 select * from zset_key where id<100 and id>1 desc;
        objects = zSetOperations.reverseRange("zset_key", 1, 100);

        //【Map】,相当于Map<K,Map<K,V>> map
        HashOperations<String,String,Object> hashOperations = redisTemplate.opsForHash();
        hashOperations.put("hash_key", "map_key", "map_value1");  //增
        hashOperations.putAll("hash_key", new HashMap<String, Object>(){{put("map_key_1", 2);}});    //增
        longValue = hashOperations.increment("hash_key", "map_key_1", 10);  //map_value += 10;   map_value不是数字会报错
        Map map = hashOperations.entries("hash_key");  //查,相当于 Map<K,V> subMap = map.get(hash_key);
        object = hashOperations.get("hash_key", "map_key");  //查,相当于 V value = map.get(hash_key).get(map_key);


        bool = redisTemplate.delete("string_key");    //删

        System.out.println(object + "," + longValue + "," + doub + "," + objects + "," + map);
    }


    /**
     * 绑定键的5种数据类型+1种扩展类型操作
     */
    public void boundHandle(){

        /**
         * 对5种数据类型的绑定,实际是绑定key之后,对key对应的value进行持续疯狂输出,为value的操作提供方便,和上面不绑定
         * 的区别是:不绑定操作每次都要指定key,非常繁琐。
         */
        //【String】
        BoundValueOperations<String, Object> boundValueOperations = redisTemplate.boundValueOps("string_key");
        boundValueOperations.append("appendString");
        boundValueOperations.set("newString");
        boundValueOperations.get();

        //【List】
        BoundListOperations<String,Object> boundListOperations = redisTemplate.boundListOps("list_key");
        boundListOperations.leftPush("value0");

        //【set】
        BoundSetOperations<String,Object> boundSetOperations = redisTemplate.boundSetOps("set_key");
        boundListOperations.range(20, 100);

        //【zset】
        BoundZSetOperations boundZSetOperations = redisTemplate.boundZSetOps("zset_key");
        boundListOperations.leftPushIfPresent("value1");   //如果存在就从左边取一个

        //【hash】
        BoundHashOperations<String,String,Object> boundHashOperations = redisTemplate.boundHashOps("hash_key");
        boundHashOperations.put("map_key", "map_value");

        /**
         * 1种扩展类型
         */
        //【BoundGeoOperations】,地理位置操作

        // BoundGeoOperations<K,M>是“坐标(K)-成员(Member)”类型的操作类,boundGeoOps("key")相当于打开一个叫key的图层,这个
        // 图层是“坐标(K)-成员(Member)”类型的集合
        BoundGeoOperations<String,Object> boundGeoOperations = redisTemplate.boundGeoOps("CHINA:CITY:FOOD");
        Point nanjing = new Point(118.803805,32.060168);
        Point beijing = new Point(116.397039,39.9077);
        //增加
        boundGeoOperations.add(nanjing, "nanjing");
        boundGeoOperations.add(nanjing, "包子?");
        boundGeoOperations.add(nanjing, "还有什么吃的");
        boundGeoOperations.add(beijing, "beijing");
        boundGeoOperations.add(beijing, "爆肚");
        boundGeoOperations.add(beijing, "涮羊肉");
        boundGeoOperations.add(beijing, "豆汁");

        //计算两个坐标之间的距离
        Distance distance = boundGeoOperations.distance("nanjing", "beijing", Metrics.KILOMETERS);
        //南京为中心,半径1000*1000M的区域的Member集合
        GeoResults<RedisGeoCommands.GeoLocation<Object>> results
                = boundGeoOperations.radius(new Circle(beijing, 1000*1000));
        //删除
        boundGeoOperations.remove("豆汁");
    }
}

  总结:RedisTemplate的API比Jedis的API更好掌握,使用起来也更直观方便。RedisTemplate是基于jedis的封装,因此效率自然比Jedis要低,至于低多少可以参考本文。另外,地理位置操作是Redis从命令层面提供的支持而非Spring提供的封装,它基于Haversine公式,适用于地球,最大偏差5%,不适用于标准球体,参考中文官网文档

4 Redis的其它特性

4.1 发布订阅

  这里声明一下,大数据量的订阅发布建议使用消息队列(MQ)。Redis的发布订阅在Spring的支持下用起来更顺手,Spring的相关支持在org.springframework.data.redis.listener包中。Redis的发布订阅有Redis命令支持,RedisTemplate发布订阅也是建立在此基础之上。下面是相关代码:
(1)编写发布者

@Component
public class Publisher {

    @Autowired
    private RedisTemplate redisTemplate;


    public boolean publish(String channel, UserEntity message){
        if (StringUtils.isEmpty(channel) || message == null){
            return false;
        }
        redisTemplate.convertAndSend(channel, message);
        System.out.println("我是发布者,发布了消息,channle:" + channel + "," + message.getAddress());
        return true;
    }
}

(2)编写订阅者
订阅者需要实现org.springframework.data.redis.connection.MessageListener接口。

@Component
@Scope("prototype")
public class Subscriber implements MessageListener {
    @Autowired
    private Jackson2JsonRedisSerializer serializer;


    @Override
    public void onMessage(Message message, @Nullable byte[] pattern) {
        Object messageBody = serializer.deserialize(message.getBody());

        if (messageBody instanceof UserEntity) {
            UserEntity userEntity = (UserEntity) messageBody;
            System.out.println("我是订阅者,收到消息:" + userEntity.getAddress());
        }
    }
}

(3)配置订阅者

@Component
public class ListenerConfig {
    @Autowired
    private Subscriber subscriberA;

    @Autowired
    private Subscriber subscriberB;

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        MessageListenerAdapter adapterA = new MessageListenerAdapter(subscriberA);
        MessageListenerAdapter adapterB = new MessageListenerAdapter(subscriberB);
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(adapterA, new ChannelTopic("test_channel_1"));
        container.addMessageListener(adapterB, new ChannelTopic("test_channel_2"));

        return container;
    }
}

  使用RedisTemplate需要配置RedisMessageListenerContainer,RedisMessageListenerContainer有一个内部类
DispatchMessageListener与监听所有订阅的消息,RedisMessageListenerContainer负责循环通知订阅者。
RedisTemplate底层使用netty,PubSubEndPoint的notifyListeners()方法是连接netty和RedisMessageListenerContainer的桥梁。

4.2 Redis事务

(1)配置事务管理器

@Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException {
        return new DataSourceTransactionManager(dataSource);
    }

(2)开启RedisTemplate事务支持

RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(serializer);
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(serializer);
        template.setHashValueSerializer(serializer);
        //打开事务支持
        template.setEnableTransactionSupport(true);
        template.afterPropertiesSet();

(3)在需要事务支持的方法上标注@Transactional注解(必须Public方法)

@Transactional
   public boolean transactionTest(boolean interrupt){}

5 总结

  依赖Redis的几种数据类型和高吞吐量特性,Redis可被用来设计成很多有用的数据结构,例如分布式锁、消息总线等,碍于篇幅,我把分布式锁、消息总线的内容转移到下一篇文章中,敬请关注,本文的GitHub地址。