Spring Boot学习笔记:Redis

151 阅读9分钟

本文内容来自Spring Boot相关书籍学习总结

Redis是目前使用广泛的缓存服务,与Memcached等缓存中间件相比,Redis支持数据持久化,支持更多的数据结构和更丰富的数据操作,此外,Redis有着高可用的集群方案和广泛的应用场景。

1、Redis入门

1.1 Redis简介

Redis的主要特点如下:

1)支持数据的持久化,可以将内存中的数据持久化保存在磁盘中,重启后再次将磁盘中的数据加载到内存。

2)丰富的数据类型,不仅支持简单的key-value类型的数据,还提供List、Set、ZSet、Hash等数据结构的存储。

3)支持数据的备份,即master-slave(主-从)模式的数据备份。

4)丰富的特性,支持publish/subscribe(发布/订阅)、通知、key过期等特性。

1.2 Redis数据类型

Redis中主要有5种数据类型:String(字符串类型)、Hash(哈希类型)、List(列表类型)、Set(集合类型)、ZSet(有序集合类型)。

数据类型说明主要方法适用场景
StringString类型是Redis中最简单、最常用的数据结构,既可以存储文字(例如"hello world"),又可以存储数字(例如整数10086和浮点数3.14),还可以存储二进制数据(例如10010100)。set、get、decr、incr、mget主键生成、数据缓存
HashHash(哈希表)是一个String类型的Field(字段)和Value(值)之间的映射表,类似于Java中的HashMap。一个哈希表由多个字段-值对(Field-Value Pair)组成,值可以是文字、整数、浮点数或者二进制数据。在同一个哈希表中,每个字段的名称必须是唯一的。hset、hget、hmget缓存用户信息
ListList(列表)类型是基于双向链表实现的,可以支持正向、反向查找和遍历。从用户角度来说,List是简单的字符串列表,字符串按照添加的顺序排序。可以添加一个元素到List列表的头部(左边)或者尾部(右边)。一个List最多可以包含2^32-1个元素(最多可超过40亿个元素)。lpush、rpush、lpop、rpop、llen标签、收藏、消息队列等
SetSet(集合)是String类型的无序集合,其实也是通过哈希表实现的。集合元素是惟一的,它会自动去掉重复元素。sadd、scard、sismember、srem共同好友等
ZSetZSet和Set一样也是String类型元素的集合,并且不允许出现重复的成员。区别在于:ZSet每个元素会关联一个double类型的分数Score,它会根据提供的score参数自动排序。zadd、zcard、zrange排行榜、关注度

2、 Spring Boot集成Redis

2.1 Spring Boot对Redis的支持

Spring Boot提供了集成Redis的组件包spring-boot-starter-data-redis,能够非常方便地集成到项目中。Spring Boot 2.0版本之后默认使用Lettuce客户端。

<!--引入redis依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
  <version>2.8.0</version>
</dependency>

添加配置文件

# 配置redis
#Redis数据库(默认0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.85.150
# Redis服务器连接端口
spring.redis.port=6379
# 连接超时时间(毫秒)
spring.redis.timeout=60s

# lettuce连接池配置
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=1000
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=10
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=2
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8

Jedis和Lettuce的区别:

Jedis在实现上直连Redis服务器,在多线程环境下是非线程安全的,除非使用连接池为每个Jedis实例增加物理连接。

Lettuce基于Netty的连接实例(StatefulRedisConnection)可以在多个线程间并发访问,并且是线程安全的,它支持多线程环境下的并发访问,同时也是可伸缩的设计,在一个连接实例不够的情况下可以按需增加连接实例。

2.2 RedisTemplate

RedisTemplate是Spring针对Redis封装的一个比较强大的模板,以方便使用。只要在所需的地方注入RedisTemplate即可,无须其他额外配置,开箱即用。

RedisTemplate有两个方法经常用到:opsForXXX()boundXXXOps(),XXX是value(值)的数据类型。opsForXXX获取到一个操作(Operation),但是没有指定操作的key(键),可以在一个连接(事务)内操作多个key以及对应的value;boundXXXOps获取到一个指定key的操作,在一个连接内只操作这个key对应的value。

如果不想用默认的序列化器,可以在Spring Boot的配置类中自定义RedisTemplate,修改RedisTemplate的序列化器

@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    //创建RedisTemplate
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    //设置连接工厂
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    //设置序列化工具
    GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    //key和hashkey使用string序列化
    redisTemplate.setKeySerializer(RedisSerializer.string());
    redisTemplate.setHashKeySerializer(RedisSerializer.string());
    //value和hashValue采用JSON序列化
    redisTemplate.setValueSerializer(jsonRedisSerializer);
    redisTemplate.setValueSerializer(jsonRedisSerializer);
    return redisTemplate;
}

Spring提供了一个StringRedisTemplate类,它的key和value的序列化方式默认是String方式。

@Bean(name = "stringRedisTemplate")
public StringRedisTemplate initStringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
    stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
    return stringRedisTemplate;
}

3、操作Redis数据结构

@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;

/**
* 字符串类型
*
* @throws JsonProcessingException
*/
@Test
public void one() throws JsonProcessingException {
    Person employee = new Person(10001, 27, "mimangkai", "迷茫凯", "shanghai");
    //获取字符串操作接口
    ValueOperations valueOperations = redisTemplate.opsForValue();
    final String key = "redis:test:1";
    final String content = objectMapper.writeValueAsString(employee);
    log.info("写入缓存中的用户实体对象信息为:{}", employee);
    valueOperations.set(key, content);
    //从缓存中读取内容
    Object result = valueOperations.get(key);
    if (result != null) {
        Person resultP = objectMapper.readValue(result.toString(), Person.class);
        log.info("读取缓存内容并反序列化后的结果:{}", resultP);
    }
}

/**
* 列表类型
*/
@Test
public void two() {
    List<Person> list = Lists.newArrayList();
    list.add(new Person(10001, 27, "mimangkai", "迷茫凯", "shanghai"));
    list.add(new Person(10002, 28, "mimangfei", "迷茫菲", "hangzhou"));
    log.info("构造已经排好序的用户对象列表:{}", list);
    final String key = "redis:test:2";
    //获取列表操作接口
    ListOperations listOperations = redisTemplate.opsForList();
    list.forEach(e -> listOperations.leftPush(key, e));
    log.info("--获取Redis中List的数据 - 从队头中获取--");
    Object res = listOperations.rightPop(key);
    Person employee;
    while (res != null) {
        employee = (Person) res;
        log.info("当前数据:{}", employee);
        res = listOperations.rightPop(key);
    }
}

/**
* 集合类型
*/
@Test
public void three() {
    List<String> userList = Lists.newArrayList();
    userList.add("mimangkai");
    userList.add("bigdata");
    userList.add("wangkai");
    userList.add("spark");
    userList.add("bigdata");
    log.info("待处理的用户姓名列表:{}", userList);
    final String key = "redis:test:3";
    //获取集合操作接口
    SetOperations setOperations = redisTemplate.opsForSet();
    userList.forEach(e -> setOperations.add(key, e));
    //从缓存中获取用户对象集合
    Object res = setOperations.pop(key);
    while (res != null) {
        log.info("当前用户:{}", res);
        res = setOperations.pop(key);
    }
}

/**
* 有序集合
*/
@Test
public void four() {
    List<PhoneUser> list = Lists.newArrayList();
    list.add(new PhoneUser("101", 120.0));
    list.add(new PhoneUser("104", 100.0));
    list.add(new PhoneUser("103", 125.0));
    list.add(new PhoneUser("102", 130.0));
    list.add(new PhoneUser("105", 70.0));
    list.add(new PhoneUser("106", 148.0));
    log.info("无序的用户手机充值列表:{}", list);
    final String key = "redis:test:4";
    //ZSet在add元素进入缓存后,下次不能进行更新,需要先清空缓存(实际生产环境中不建议这么使用)
    redisTemplate.delete(key);
    //获取有序集合操作接口
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    list.forEach(e -> zSetOperations.add(key, e, e.getFare()));
    //获取充值排名靠前的用户列表
    Long size = zSetOperations.size(key);
    //从小到大排序
    Set<PhoneUser> resSet = zSetOperations.range(key, 0L, size);
    //从大到小排序
    //        Set<PhoneUser> resSet = zSetOperations.reverseRange(key, 0L, size);
    resSet.forEach(e -> log.info("当前记录:{}", e));
}

/**
* 哈希存储
*/
@Test
public void five() {
    List<Student> students = Lists.newArrayList();
    List<Fruit> fruits = Lists.newArrayList();
    students.add(new Student(10010, "mimangkai", "迷茫凯"));
    students.add(new Student(10011, "mimangfei", "迷茫菲"));
    students.add(new Student(10012, "mimangwei", "迷茫尉"));
    fruits.add(new Fruit("apple", "红色"));
    fruits.add(new Fruit("orange", "橙色"));
    fruits.add(new Fruit("banana", "黄色"));
    final String sKey = "redis:test:5";
    final String fKey = "redis:test:6";
    //获取哈希存储操作接口
    HashOperations hashOperations = redisTemplate.opsForHash();
    students.forEach(s -> hashOperations.put(sKey, s.getId(), s));
    fruits.forEach(f -> hashOperations.put(fKey, f.getName(), f));
    //获取对象列表
    Map<String, Student> sMap = hashOperations.entries(sKey);
    log.info("获取学生对象列表:{}", sMap);
    Map<String, Student> fMap = hashOperations.entries(fKey);
    log.info("获取水果对象列表:{}", fMap);
    String sField = "10012";
    Student student = (Student) hashOperations.get(sKey, sField);
    log.info("获取指定的学生对象:{}->{}", sField, student);
    String fField = "banana";
    Fruit fruit = (Fruit) hashOperations.get(fKey, fField);
    log.info("获取指定的水果对象:{}->{}", fField, fruit);
}

/**
  * key过期失效
  * @throws InterruptedException
  */
@Test
public void six() throws InterruptedException {
    final String key1 = "redis:test:7";
    ValueOperations valueOperations = redisTemplate.opsForValue();
    valueOperations.set(key1, "expire操作", 10L, TimeUnit.SECONDS);
    Thread.sleep(5000L);
    Boolean existKey1 = redisTemplate.hasKey(key1);
    Object value = valueOperations.get(key1);
    log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);
    Thread.sleep(5000L);
    existKey1 = redisTemplate.hasKey(key1);
    value = valueOperations.get(key1);
    log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);
}

@Test
public void seven() throws InterruptedException {
    final String key2 = "redis:test:8";
    ValueOperations valueOperations = redisTemplate.opsForValue();
    valueOperations.set(key2, "expire操作-2");
    redisTemplate.expire(key2, 10L, TimeUnit.SECONDS);
    Thread.sleep(5000L);
    Boolean existKey2 = redisTemplate.hasKey(key2);
    Object value = valueOperations.get(key2);
    log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey2, value);
    Thread.sleep(5000L);
    existKey2 = redisTemplate.hasKey(key2);
    value = valueOperations.get(key2);
    log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey2, value);
}

4、工具类封装

/**
 * redis工具类
 */
@Component
public class RedisOperator {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // Key(键),简单的key-value操作

    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public boolean keyIsExist(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     *
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 实现命令:expire 设置过期时间,单位秒
     *
     * @param key
     * @return
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 实现命令:increment key,增加key一次
     *
     * @param key
     * @return
     */
    public long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 实现命令:decrement key,减少key一次
     *
     * @param key
     * @return
     */
    public long decrement(String key, long delta) {
        return redisTemplate.opsForValue().decrement(key, delta);
    }

    /**
     * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 实现命令:DEL key,删除一个key
     *
     * @param key
     */
    public void del(String key) {
        redisTemplate.delete(key);
    }

    // String(字符串)

    /**
     * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
     *
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
     *
     * @param key
     * @param value
     * @param timeout (以秒为单位)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     *
     * @param key
     * @param value
     */
    public void setnx60s(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     *
     * @param key
     * @param value
     */
    public void setnx(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 实现命令:GET key,返回 key所关联的字符串值。
     *
     * @param key
     * @return value
     */
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    /**
     * 批量查询,对应mget
     *
     * @param keys
     * @return
     */
    public List<String> mget(List<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 批量查询,管道pipeline
     *
     * @param keys
     * @return
     */
    public List<Object> batchGet(List<String> keys) {

//    nginx -> keepalive
//    redis -> pipeline

        List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                StringRedisConnection src = (StringRedisConnection) connection;

                for (String k : keys) {
                    src.get(k);
                }
                return null;
            }
        });

        return result;
    }


    // Hash(哈希表)

    /**
     * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
     *
     * @param key
     * @param field
     * @param value
     */
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
     *
     * @param key
     * @param field
     * @return
     */
    public String hget(String key, String field) {
        return (String) redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
     *
     * @param key
     * @param fields
     */
    public void hdel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
     *
     * @param key
     * @return
     */
    public Map<Object, Object> hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List(列表)

    /**
     * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long lpush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 实现命令:LPOP key,移除并返回列表 key的头元素。
     *
     * @param key
     * @return 列表key的头元素。
     */
    public String lpop(String key) {
        return (String) redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

}