Redis 基础知识汇总

RedisTemplate 最佳使用方式

Posted by sincosmos on October 9, 2019

基本数据结构

参考资料 Redis 数据结构基础教程

string

string 是一个可变的字节数组,常见的操作有 set, get, strlen, getrange, setrange, append, ttl, expire, del. 如果存入的是整数,还可以作为计数器使用,incrby, decrby, incr, decr. 使用 setbit, getbit, bitcount, bitop (bitop and/or/xor destkey key [key…]) 命令可以将 string 类型作为 bit map 使用。
string 底层是一个可变的字节数组,内部结构类似于 Java 的 ArrayList,采用预先分配冗余空间的方式来减少由于字符串变更带来的内存重新分配。当字符串长度小于 1M 时,扩容都是加倍现有的空间。字符串长度最大是 512 M。

list

redis 列表结构为 list,使用双向链表结构保存列表。因为是双链表,list 即可作为队列(lpush/rpop, rpush/lpop)使用,也可以作为堆栈(rpush/rpop, lpush/lpop)使用。用 list 可以实现 redis 消息队列( redis 另外提供了发布/订阅机制实现消息队列)。list 的 push 支持一次操作多个元素(实际 push 是依次一个一个进行的),pop 操作一次只能弹出一个元素。
list 常见的其它操作有 llen (list length), lindex (get element by index), lrange (get elements by range of indices), lset (set element by index), linsert before/after (insert element before/after sepcified element), lrem count (if count > 0, search from head, remove count of elements; if count < 0, search from tail, remove count of elements; if count = 0, remove all elements), ltrim (维护定长列表,除了范围内的元素外,其它元素将被删除)。
redis 底层使用 ziplist 和 quicklist 来存储链表结构。ziplist 将类用连续空间保存链表元素,类似 array,只不过对其中的元素进行一层包装。quicklist 是 ziplist 和 linkedlist 二者的结合,quicklist 的元素是 ziplist,亦即 quicklist 是 ziplist 组成的双向链表。

hash

hash 等价于 java 中的 HashMap,存储的是键值对。使用 hset (hset v_name key1 value 一次加入一个键值对)或 hmset(hmset v_name key2 value2 key3 value3… 一次加入多个键值对)。hdel 支持一次删除一个或多个键。hget 获得键对应的值,hexists 判断键是否存在于 hash 中。如果键对应的值是整数,我们也可以将整数作为计数器使用,相关的命令有 hincrby, hdecrby。hgetall 得到 hash 中所有的键值对。 hash 的底层存储结构也类似 HashMap,使用链表解决 hash 冲突。 当 hash 碰撞频繁时,就需要对其进行扩容,扩容需要申请原始 hash 数组 2 倍大小的空间,然后将所有的键值对重新分配到新的数组下标对应的链表中。未来避免扩容占据过多的线程执行时间,redis 在元素较多时采用渐进式的 rehash 方案,它会同时保留新旧两个 hash 结构,在后续的定时任务以及读写指令中将旧 hash 中的元素逐步迁移到新的结构中。另外,和 HashMap 不同,redis 的 hash 结构支持缩容。

set

类似 java, redis set 的内部实现也是基于上面的 hash,所有的键指向同一个内部值。常见的命令有 sadd (增加一个或多个元素),members (获得所有元素),scard (获得元素个数),srandmember v_name [count](随机获取 count 个元素,如果省略 count,随机取一个元素),srem v_name key1 key2 (删除一个或多个元素),spop(随机删除一个元素),sismember (判断元素是否在 set 中)。
set 底层也是用的 hash 结构,所有的 value 都指向同一个内部值。

zset

zset 是 redis 提供的有序集合,可以将其理解为 map<key, score>,除了 hash 结构,为了提高增删查改的效率,底层还使用了 skiplist 结构,skiplist 按照 score 进行排序,score 可以是整数或浮点数。
常用的命令有 zadd v_name score key (增加一个或多个元素),zcard (获取元素个数),zrem (删除一个或者多个元素),zincrby v_name 1.2 key1 (作为计数器使用),zscore (获取对应 key 的权重),zrank (获得元素的正向排名),zrevrank(获得元素的倒数排名),zrange v_name start end [withscores](获得正向排名从 start 到 end 的所有元素,可选是否同时获得元素对应的 scores),zrevrange 和 zrange 类似,只不过它按负向排名输出元素。zrangebyscore v_name score_start score_ed [withscores] 和 zrevrangebyscore 获取指定 score 范围内的元素。zremrangebyrank 和 zremrangebyscore 分别删除指定排名范围内和权重范围内的元素。 zset 底层使用 hash 结构的同时,为了提高读写效率,它还采用 skiplist 这样的数据结构。hash 结构用来关联 key 和 score,skiplist 对 score 进行排序后在元素上添加多层链表指针。跳跃链表次采用一个随机策略来决定新元素最高拥有第几层的指针。

对于大集合结构,不推荐使用 hgetall、lrange、smembers、zrange、sinter 进行操作,有遍历需求时,推荐使用 hscan, sscan, zscan 等操作替代。

发布/订阅模式

1) subscribe channel [channel …],订阅给定的频道列表
2) publish channel message ,将消息发送到指定的频道
3) unsubscribe channel [channel …],退订给定的频道列表
4) psubscribe pattern [pattern …],订阅符合给定模式的频道列表
5) punsubscribe pattern [pattern …],退订符合给定模式的频道列表
6) pubsub [argument [argument ...]] 查看订阅与发布系统状态,它由数个不同格式的子命令组成。

分布式锁

1) setex key timeout(s) value,设置 key 并并设置过期时间,整个过程是原子操作
2) setnx key value, 当指定的 key 不存在时,为 key 设置指定的值。返回 1 则说明当前线程获得了分布式锁;返回 0 则说明其它线程已经获得了锁
3)getset key value, 将 value 赋值给 key,并返回旧值,如果旧值不存在返回 nil,如果旧值不是字符串类型,返回错误

部署

Redis 服务主要有四种部署方式。参考What Redis deployment do you need? 1) Redis Standalone,即单机服务。优点是简单;缺点是高可用能力差、扩展性差。
2) Redis Master and Slaves, 即主从模式,一般是主节点提供写服务,并及时将数据同步到从节点,从节点提供读服务。优点是可用性高,主节点故障可以快速将某一个从节点升级为主节点;另外主从分离、增多从节点可以有效应对大并发量操作,提升性能。缺点是提供高可用性需要另行开发主从自动切换功能或故障时手动切换,主节点切换后还需考虑使用方发现新的主节点并进行写入的问题;主节点写入能力受到单机的限制时,还要考虑数据分片策略;旧版本 Redis 原生的主从同步可能会有问题。 3) Redis Sentinel,即哨兵模式。 哨兵模式
该模式是 Redis 社区版本官方提供的高可用解决方案,部署时除了 Redis 数据集群,还需要部署 Redis Sentinel 集群。Redis Sentinel 自身相当于一个高可用的 Redis 服务治理集群,至少需要 3 个节点(节点数必须为奇数),Sentinel 集群负责监控 Redis 数据集群的当前 Master 节点,并提供故障时的自动主从切换功能。优点是高可用,稳定,资源消耗少,高可用。缺点是客户端需要支持和 Sentinel 进行通信的功能;数据集群中的从节点不提供读写功能,一方面造成资源浪费,另一方面导致扩展性差。 4) Redis Cluster,Redis 分布式集群。 分布式集群
Redis 社区版本官方推出的分布式集群解决方案,多主多从(至少3主3从),主节点提供读写操作,从节点作为备用节点,不对客户端提供服务,只作为故障转移备用机。主节点之间是去中心化的,数据根据键值 Hash 映射到 0~16383个整数桶内,每个节点负责一部分桶,类似一致性 Hash。

使用 docker 部署 redis 服务

可以使用 docker 在主机上启动 redis 服务。

docker run -p 6379:6379 --mount source=/data,taregt=/data -d redis --appendonly yes

上述命令会自动拉取 redis 最新 docker 镜像并在本机启动 redis 服务,并将容器的 6379 端口与主机 6379 端口映射,其它应用可以无感知的在本机使用 redis 服务。容器启动后,可以通过如下命令启动 redis 客户端。

docker exec -it container-id redis-cli

如果要从其它容器访问上面的 redis 服务,可以使用如下命令。其中 network 指定客户端容器的网络,-h 指定服务器 IP,如果服务器不是默认端口 6379 那么还需要使用 -p 指定服务器端口。

docker run -it --network host --rm redis redis-cli -h localhost -p 6379

客户端

Lettuce 是非阻塞的 Redis java 客户端。在项目中引入了 spring-boot-starter-data-redis 后,默认会实例化两个 RedisTemplate 对象:RedisTemplate 和 StringRedisTemplate,其中默认的连接客户端就是上述 Lettuce。

// class name: RedisAutoConfiguration
//注意 ConditionalOnMissingBean注解,它的意义是当没有自定义相应的 bean 时才会被生成
//如果自定义了同类型的 redisTemplate 对象,则不会生成这里默认的 bean
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
		RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
	RedisTemplate<Object, Object> template = new RedisTemplate<>();
	template.setConnectionFactory(redisConnectionFactory);
	return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(
		RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
	StringRedisTemplate template = new StringRedisTemplate();
	template.setConnectionFactory(redisConnectionFactory);
	return template;
}

Lettuce 底层与 Redis 的交互是以二进制 byte[] 方式进行的。为了支持 Java 中的数据类型,需要对传输的数据进行序列化和反序列化操作。Lettuce 与 Redis 交互需要传输的数据包括 key 和其对应的存储数据,key 是字符串类型,存储数据可以是 string, list, hash, set, zset 等数据类型对。于 hash 结构来说,存储数据又包括 hashkey 和 hashvalue 两部分。因此,RedisTemplate 实例需要设定对 key, value, hashKey, hashValue 进行序列化和反序列化的方式。
一般来说,java 应用中,key 使用 StringRedisSerializer 序列化类,value, hashKey, hashValue 使用 JdkSerializationRedisSerializer。使用 JdkSerializationRedisSerializer 读/写 redis 数据时,数据中额外包含 java 特定的类信息,如果 java 读/写的数据是要与非 java 应用进行交互的,则不能使用 JdkSerializationRedisSerializer,此时可以使用 GenericJackson2JsonRedisSerializer 对数据进行 JSON 格式的读/写。 RedisTemplate 操作 Redis 中不同的数据类型,需要使用不同的算子,opsForValue 操作 redis 中 string 类型的数据,opsForList 操作列表数据,opsForHash 操作 hash 类型的数据等。
另外,需要注意 RedisTemplate<K, V> 的两个泛型参数和序列化器之间的关系。K, V 这两个泛型参数,本意是限制 get 操作的入参 key 类型为泛型 K,返回值类型为泛型 V 等,但由于实际限制 key 类型的地方是 keySerializer,限制返回值 value 类型的地方是 valueSerializer,所以这两个泛型参数的限制并不是绝对的。例如下面的示例代码中,我们自定义了 redisTemplate,泛型参数为 <String, String> 类型。

public RedisTemplate<String, String> redisTemplate() {
    RedisTemplate<String, String> template = new RedisTemplate<>();
    // need a RedisConnectionFactory
    template.setConnectionFactory(redisConnectionFactory());

    RedisSerializer<Integer> keySerializer = new RedisSerializer<Integer>() {
        @Override
        public byte[] serialize(Integer integer) throws SerializationException {
            return String.valueOf(integer).getBytes();
        }

        @Override
        public Integer deserialize(byte[] bytes) throws SerializationException {
            return Integer.valueOf(new String(bytes));
        }
    };
    JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();

    template.setKeySerializer(keySerializer);
    template.setHashKeySerializer(jdkSerializationRedisSerializer);
    template.setValueSerializer(new StringRedisSerializer());
    template.setHashValueSerializer(jdkSerializationRedisSerializer);

    template.setEnableTransactionSupport(true);
    template.afterPropertiesSet();

    return template;
}
    

如果我们 autowired 上面的 redisTemplate Bean,因为它的 keySerializer 实际的泛型参数是 Integer 类型,所以我们实际 get 操作的入参就是 Integer 类型,而不是 RedisTemplate 泛型里的 String 类型。

@Autowired
private RedisTemplate<String, String> redisTemplate;
    
ValueOperations<String, String> t = redisTemplate.opsForValue();
String intKey1 = t.get(100);    

String intKey2 = redisTemplate.opsForValue().get(100);

默认实例化的 stringRedisTemplate

默认实例化的 stringRedisTemplate 将 key, value, hashKey, hashValue 的序列化器都设置为 StringRedisSerializer。 前面说过,Lettuce 底层与 Redis 的交互是以二进制 byte[] 方式进行的,stringRedisTemplate 的 get 类操作只接受 String 类型的 key 作为入参,内部直接使用 string.getBytes(charset) 将其转化为 byte[] 类型。 stringRedisTemplate 的 get 类操作使用 new String(bytes, charset) 将从 redis 得到的 byte[] 类型转换为 String 类型返回。stringRedisTemplate 的 set 类操作与 get 类似,也只接受 String 类型的入参。

默认实例化的 redisTemplate

默认实例化的 redisTemplate 将 key, value, hashKey, hashValue 的序列化都设置为 JdkSerializationRedisSerializer。默认实例化的 redisTemplate 的 get 类操作接受任何实现了 Serializable 接口的 Java 类实例(将基本类型自动装箱)的 key 作为入参,内部最终使用 ObjectOutputStream 类的方法将 java 类实例转换为 byte[] 流。默认实例化的 redisTemplate 的 get 类操作最终使用 ObjectInputStream 将从 redis 得到的 byte[] 类型转换为 Object 类型返回。需要注意的是,ObjectOutputStream 会在其生成的 byte[] 流起始附加上 java 类信息,同时 ObjectInputStream 解析的 byte[] 必须有 java 类信息才能正确解析。因此 JdkSerializationRedisSerializer 只适用于 java 应用内部读/写 Redis。

RedisTemplate “最佳”使用方式

我们来分析一下以下几种场景下,怎么使用 RedisTemplate 的实例从 Redis 中存取数据。
1) redis 中的数据 key: “a”, value: “astring” key: “b”, value: “100” key: “c”, value: [“c1”, “c2”, “c3”]

String a = stringRedisTemplate.opsForValue().get("a"); //a = "astring"
String b = stringRedisTemplate.opsForValue().get("b"); //b = "100"
//下面的语句无法通过编译,因为stringRedisTemplate get 操作返回的只能是 String 类型
//Integer b = stringRedisTemplate.opsForValue().get("b");

//下面的语句,能正常执行,但是得到 a 的值是 null,因为 redisTemplate 使用 JdkSerializationRedisSerializer 
//将被检索的 key 进行序列化,生成的二进制流里会含有 java.lang.String 这样的类信息,导致最终向 redis 查询的 key
//并不是 "a",而是 "\xac\xed\x00\x05t\x00\x01c" 这样的 key,导致值为空
//String a = (String) redisTemplate.opsForValue().get("a");

List<String> c = stringRedisTemplate.opsForList().get("c", 0, 1); //c = list of "c1", "c2"
//下面的语句,不能正常执行,opsForValue 不能操作 list 类数据
//String c = stringRedisTemplate.opsForValue().get("c");

2) 先写后读

stringRedisTemplate.opsForValue().set("x", "xstring");
stringRedisTemplate.opsForValue().set("y", "1000");
stringRedisTemplate.opsForList().leftPushAll("z", "z1", "z2", "z3");

redisTemplate.opsForValue().set("p", "pstring");
redisTemplate.opsForList().leftPushAll("q", "q1", "q2");

String p = (String) redisTemplate.opsForValue().get("p");
// 下面获得的 p 的值是 null,原因和上面一样
//String p = stringRedisTemplate.opsForValue().get("p");

可以看出来,如果用 stringRedisTemplate 往 Redis 存入数据,值只能是 String 类型,如果要的数据是 POJO,是比较困难的。如果使用 默认的 redisTemplate,key 和 value 都会携带上类信息,外部程序是不可读的。 3) 在我们的项目中,Java 从 Redis 读取的数据是其它程序写入到 Redis 的,值的类型可能是简单的字符串或数字,也可能是 JSON 格式的字符串。同时,Java 应用程序也可能要写入数据供其他程序使用,数据可以是简单的字符串或数字,也可能是 JSON 格式的字符串。 ** 注意,使用缓存在不同应用程序间进行数据共享是否合理存疑 ** 简单来说,读取我们可以直接 stringRedisTemplate 获得相应的 JSON 字符串,然后使用 JSON 工具将其转换为 POJO 对象。写进 Redis 时,也可以使用 JSON 工具先将 POJO 转换为 JSON 对象,然后将 JSON 对象转换为 JSON 字符串写入 Redis。
更加好的方式是,我们自定义一个 RedisTemplate<String, String> 实例,注意这两个泛型参数。