1,Redis概述
1.1,Redis基本概念
在传统的Java Web项目中,使用数据库进行存储数据,但是有一些致命的弊端,这些弊端主要来自于性能方面。比如一些商品抢购的场景,或者是主页访问量瞬间较大的时候,一瞬间成千上万的请求就会到来,需要系统在极短的时间内完成成千上万次的读/写操作,这个时候往往不是数据库能够承受的,极其容易造成数据库系统瘫痪,最终导致服务宕机的严重生产问题。
Redis是一个开源、单线程、内存操作的key-value数据库,key-value数据库会以key-value对的方式存储数据,其内部通常采用哈希表这种结构来记录数据。在使用时,通过key来读取或写入相应的数据,具有极高的效率,因此比处理单条数据的CRUD时非常高效。
key-value数据库的优势同时也是缺陷:它只能通过key来访问数据,数据库并不知道每条数据的其他信息(只有key)。因此,如果试图通过条件筛选数据,key-value数据库就会变得非常低效了。
注意:Redis单线程是指对Redis内的数据操作(读/写)是单线程的,比如超卖问题,有两个请求A和B都想要购买同一件商品,A请求读->B请求读->A请求写->B请求写(对于单个请求读/写是串行的,但是从整体看仍是并发的,所以需要加分布式锁)。
Redis作为一个优秀的key-value数据库,它的性能十分优越,可以支持每秒十几万次的读/写操作,其性能远超数据库,并且支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,而更让我们感到欣喜的是它还能支持一定的事务能力,这在高并发访问的场景下保证数据安全和一致性特别有用。
Redis的性能优越主要来自于5个方面:
- 它是基于ANSIC语言编写的,接近于汇编语言的机器语言,运行十分快速。
- 它是基于内存的读/写,速度自然比数据库的磁盘读/写要快得多。
- 它的数据库结构只有6种数据类型,数据结构比较简单,因此规则较少,而数据库则是范式,完整性、规范性需要考虑的规则比较多,处理业务会比较复杂。
- 它支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 支持主从复制,主节点会自动将数据同步到从节点,可以进行读写分离。
Redis的速度优越主要来自于5个方面:
Redis是单线程模型:Redis采用单线程模型,所有的请求都在一个线程中处理,避免了多线程之间的上下文切换和锁竞争,降低了系统开销和延迟。
Redis采用基于内存的数据存储:Redis将所有的数据都存储在内存中,避免了传统关系型数据库频繁的磁盘I/O操作,因此Redis具有更快的读写速度和更低的延迟。
Redis采用非阻塞I/O多路复用技术:Redis采用了非阻塞I/O多路复用技术,可以在单线程中处理多个连接,从而避免了线程切换带来的开销,提高了系统的并发处理能力。
Redis的网络通信采用高性能协议:Redis的网络通信采用的是RESP协议,这个协议的设计是为了提高Redis在网络中的性能和可扩展性。RESP协议采用二进制流格式传输数据,可以减少传输数据的大小,从而降低了网络带宽和传输延迟。
Redis支持多种高效数据结构:如字符串、列表、哈希、集合等,每种数据结构都针对不同的应用场景做了优化,可以提供更高效的数据操作接口。
Redis的缺点:
- 对结构化查询的支持比较差。
- 数据库容量受到物理内存的限制,不适合用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的操作,但是其功能毕竟是有限的,不如数据库的 SQL 语句强大,支持更为复杂的计算。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
- NoSQL 并不完全安全稳定,由于它基于内存,一旦停电或者机器故障数据就很容易丢失数据,其持久化能力也是有限的,而基于磁盘的数据库则不会出现这样的问题。
- 最后,其数据完整性、事务能力、安全性、可靠性及可扩展性都远不及数据库。
【Redis的底层IO模型】Redis采用epoll作为多路复用的实现机制,将所有的IO操作封装在一个线程中,使用epoll_wait()函数监听多个文件描述符,当有可读或可写事件发生时,将事件保存在队列中,并通过专门的线程处理这些事件。同时,Redis还通过设置并发连接数限制,避免出现线程和文件描述符管理过多的情况,确保了系统的稳定性。此外,Redis还支持异步IO,即当进行阻塞IO操作时,会将该操作放入到一个任务队列中,由专门的线程进行处理,避免了阻塞IO对整个系统的影响,提高了系统的性能表现。
1.2,Redis的应用场景
一般而言Redis在Java Web应用中存在两个主要的场景,一个是缓存常用的数据,另一个是在需要高速读/写的场合使用它快速读/写。
(1)缓存:在对数据库的读/写操作中,现实的情况是读操作的次数远超写操作,一般是1:9到3:7的比例,所以需要读的可能性是比写的可能性多得多。当发送SQL去数据库进行读取时,数据库就会去磁盘把对应的数据索引回来,而索引磁盘是一个相对缓慢的过程。如果把数据直接放在运行在内存中的Redis服务器上,那么不需要去读/写磁盘了,而是直接读取内存,显然速度会快得多,并且会极大减轻数据库的压力。
而使用内存进行存储数据开销也是比较大的,因为磁盘可以是TGB级别,而且十分廉价,内存一般是几百个 GB 就相当了不起了,所以内存虽然高效但空间有限,价格也比磁盘高许多,因此使用内存代价较高,并不是想存什么就存什么,因此应该考虑有条件的存储数据。一般而言,存储一些常用的数据,比如用户登录的信息;一些主要的业务信息,比如银行会存储一些客户基础信息、银行卡信息、最近交易信息等。
如果业务数据写次数远大于读次数没有必要使用Redis。如果是读次数远大于写次数,则使用Redis就有其价值了,因为写入 Redis 虽然要消耗一定的代价,但是其性能良好,相对数据库而言,几乎可以忽略不计。
(2)高速读/写场合:在互联网的应用中,往往存在一些需要高速读/写的场合,比如商品的秒杀,抢红包,淘宝、京东的双十一活动或者春运抢票等。这类场合在一个瞬间成千上万的请求就会达到服务器,如果使用的是数据库,一个瞬间数据库就需要执行成千上万的SQL,很容易造成数据库的瓶颈,严重的会导致数据库瘫痪,造成Java Web系统服务崩溃。
在这样的场合的应对办法往往是考虑异步写入数据库,而在高速读/写的场合中单单使用Redis去应对,把这些需要高速读/写的数据,缓存到Redis中,而在满足一定的条件下,触发这些缓存的数据写入数据库中。
1.3,部署方案
安装Redis:Redis的正式发布版不是安装程序,也不是可执行程序,直接是源代码,因此需要自行下载Redis源代码,然后编译成对应平台的程序。(下载地址)
#创建目录 mkdir -p /usr/local/bin/redis #cd到目录 cd /usr/local/bin/redis #下载 wget https://download.redis.io/releases/redis-6.2.5.tar.gz #解压 tar -xzvf redis-6.2.5.tar.gz #安装 cd redis-6.2.5 make
执行完make命令后,在redis-6.2.5 的 src目录下会出现编译后的 redis 服务程序 redis-server,还有用于测试的客户端程序 redis-cli 。先运行redis-server后打开新窗口运行redis-cli。
Redis默认绑定6379端口(可通过redis.conf修改),这种方式被称为“单机模式”。
Redis配置远程连接:使用 vi 或 vim 命令打开 redis.conf 配置文件,位置在Redis的根目录下。
- 默认 bind 127.0.0.1 是没有注释的,如果你要开始远程连接可以注释 # 他,或者指定IP。
- 默认 protected-mode yes 保护模式时开启的,如果你需要远程连接请将他设置为 protected-mode no。
- 带配置文件方式启动:
./src/redis-server redis.conf
关闭Redis:
ps aux|grep redis kill -9 进程号
安装Redis可视化工具:Another Redis DesTop Manager
官网地址:Releases · qishibo/AnotherRedisDesktopManager · GitHub
安装方式:默认安装
1.4,Redis淘汰机制
【问题】MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据? 当服务器内存有限时,如果大量地使用缓存键且过期时间设置得过长就会导致 Redis 占满内存,Redis的写命令会返回错误信息(但是读命令还可以正常返回)。也可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
具体的设置方法为:修改配置文件的maxmemory参数,限制Redis最大可用内存大小(单位是字节),当超出了这个限制时Redis会依据maxmemory-policy参数指定的策略来删除不需要的键直到Redis占用的内存小于指定内存。
规则 描述 volatile-lru 使用LRU(最近最少使用)算法删除一个键(只对设置了过期时间的键) allkeys-lru 使用LRU算法删除一个键 volatile-random 随机删除一个键(只对设置了过期时间的键) allkeys-random 随机删除一个键 volatile-ttl 删除过期时间最近的一个键 noeviction 不删除键,只返回错误。 【过期键删除策略】
- 定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时 器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:当一个键过期时,它不会立即被删除,而是等待该键被访问时再进行删除并返回不存在。这种方式省去了定期删除带来的CPU压力,提高了Redis服务器的响应时间和指令吞吐量。
- 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。如果请求进来,且正好服务器正在进行过期键扫描,那么需要等待 25 毫秒,如果客户端设置的超时时间小于 25 毫秒,那就会导致链接因为超时而关闭,就会造成异常。这是因为 Redis 的单线程模型只能处理一个请求,不能同时处理多个请求。
2,Redis命令
2.1,数据类型
Redis是一个key-value数据库(就像一个功能增强版的Map,且能将数据永久保存在磁盘上),总体来说用起来不难;Redis也可作为一个快速、稳定的发布/订阅系统使用 Redis使用key-value结构来保存数据,其中value支持如下5种基础数据类型:
数据类型 数据类型存储的值 说明 String 字符串、整数、浮点数;512M 可以对字符串进行操作,比如增加字符串或者求子串;如果是整数或浮点数,可以实现计算,如自增。 List 元素是String类型的有序集合,集合中的元素可以重复。 Redis支持从链表的两端插入或者弹出节点,或者通过便宜对它进行裁剪;还可以读取一个或者多个节点,根据条件删除或者查找节点。 Set 元素是String类型的无序集合,集合中的元素不能重复。 可以新增、读取、删除单个元素;检测一个元素是否在集合中;计算它和其他集合的交集、并集和差集等;随机从集合中读取元素。 Hash 是key-value集合(类似于Java的Map),key和value都是String类型数据。这种类型主要用于保存对象。 可以增、删、查、改单个键值对,也可以获取所有的键值对。 ZSet 元素是String类型的有序集合,集合中的元素不能重复。 可以增、删、查、改元素,根据分值的范围或者成员来获取对应的元素。 另外还有3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
2.2,key相关命令
【key相关命令】
- del key:删除key对应的key-value对。
- dump key:导出key对应的值。
- exists key:判断key是否存在。
- expire key seconds:设置key对应的key-value对经过seconds秒后过期。
- expireat key timetamp:设置key对应的key-value对到timestamp时过期。
- pexpire key milliseconds:设置key对应的key-value对经过milliseconds毫秒后过期。
- pexpireat key milliseconds-timestamp:设置key对应的key-value对到milliseconds-timestamp时过期。
- keys pattern:返回匹配pattern的所有key。
符号 含义 ? 匹配一个字符 * 匹配任意个(包括0个)字符 [ ] 匹配括号间的任一字符,可以使用“-”符号表示一个范围,如a[b-d]可以匹配ab、ac、ad \x 匹配字符x,用于转义符号。
- move key db:将指定key移动到db数据库中。
- persist key:删除key的过期时间,key将持久保存。
- pttl key:以毫米为单位返回一个随机的key。
- ttl key:以秒为单位返回指定key剩余的过期时间。
- randomkey:从当前数据库返回一个随机的key。
- rename key newkey:将key重命名为newkey。
- renamenx key newkey:相当于安全版的rename,仅当newkey不存在才能重命名。
- type key:返回指定key存储的数据类型。
【问题】keys命令存在的问题?
【答案】redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是
O(1)
,但是要真正实现keys的功能,需要执行多次scan。scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
2.3,String相关命令
【String相关命令】
- set key value:设置key-value对。
- get key:返回指定key对象的value。
- getrange key start end:获取指定key对应的value从start到end的字符串。
- getset key value:为指定key设置新的value,并返回原来的value。
- setex key seconds value:设置key-value对,并设置过期时间为seconds秒。
- setnx key value:set的安全版本,只有当key不存在时才能设置该key-value对。
- setrange key offset value:设置和覆盖指定key对应的value,从原有value的offset个字符开始;如果key不存在,则将前offset个字符设为空。
- strlen key:获取key对应的value的字符串长度。
- psetex key milliseconds value:SETEX的毫秒版本,过期时间以毫秒计算。
- incr key:将指定key中存储的整数值+1。
- incrby key increment:将指定key中存储的整数值增加increment整数值。
- decr key:将指定key中存储的整数值-1。和incr等命令只能用于操作数值型字符串。
- decrby key decrement:将指定key中存储的整数值减少decrement整数值。
- append key value:在指定key对应的字符串后追加新的value内容。
2.4,List相关命令
【List相关命令】List代表有序的集合,可通过命令为List添加或删除元素,List最多可包含 个元素。实际上,Redis的List也具有队列的性质,因此它包含了LPUSH、LPOP、RPUSH、RPOP等命令。
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的(和从只有20个元素的列表中获取头部或尾部的10条记录的速度是一样的)。
【常用方法】
- lindex key index:获取key对应list的index处的元素。
- linsert key before|after pivot value:在key对应的list的pivot元素之前或之后插入新的value元素。
- llen key:返回key对应的list的长度。
- lpop key:弹出并返回key对应的list的第一个元素。
- lpush key value[value ...]:向key对应的list的头部(左)添加一个或多个元素。
- lpushx key value:lpush的安全版本,仅当key对应的list存在时有效。
- lrem key count value:从key对应的value中删除count个value元素。如果count大于0,则从左边向右边删除count个元素;如果count小于0,则从右边向左边删除count各元素;如果count等于0,则删除所有元素。
- lset key index value:将key对应的List的index处元素改为value。
- ltrim key start stop:修剪list,只保留key对应的list中从start到stop之间的元素。
- rpop key:弹出并返回key对应的list的最后一个元素。
- rpoplpush source destination:弹出source的最后一个元素,添加到destination的左边(头部),并返回该元素。
- rpush key value[value... ]:向key对应的list的右边(尾部)添加一个或多个元素。
- rpushx key value:rpush的安全版本,仅当key对应的list存在时有效。
- blpop key[key...] timeout:lpop的阻塞版本。弹出并返回多个list的第一个元素,如果某个list没有元素,该命令会阻塞进程,直到所有list都有元素弹出或超时。该命令的B代表Block。
- brpop key[key...] timeout:rpop的阻塞版本。
- brpoplpush source destination timeout:rpopllpush的阻塞版本,如果source中没有元素,该命令会阻塞进程,直到source有元素弹出或超时。
2.5,Set相关命令
【Set相关命令】Set代表无序、元素不能重复的集合,因此Set中的元素都是唯一的。Set最多可以包含 个元素。Set底层其实是通过Hash表实现的,因此它的删除、查找和复杂度都是 ,性能很好。
【常用方法】
- sadd key member[member...]:向key对应的Set中添加一个或多个元素。
- scard key:返回key对应的Set中元素的个数。
- sdiff key[key...]:计算多个Set之间的差值。
- sdiffstore destination key[key...]:sdiff的存储版本,将多个set之间的差值保存到destination中。
- sinter key[key...]:返回给定Set的交集。
- sinterstore destination key[key...]:sinter的存储版本,将给定Set的交集保存到destination中。
- sismember key member:判断member是否为key对应的set元素。
- smembers key:返回key对应的Set的全部元素。
- smove source destination member:将source中的member元素移到destination中。
- spop key:弹出key对应Set中随机一个元素。
- spandmember key[count]:返回key对应的Set中随机的count个元素(不删除元素)。
- srem key member[member...]:删除key对应的Set中的一个或多个元素。
- sunion key[key...]:计算给定Set的并集。
- sunionstore destination key[key...]:sunion的存储版本,将给定Set的并集保存到destination中。
- sscan key cursor[MATCH pattern][COUNT count]:使用cursor遍历key对应的Set。pattern指定值遍历匹配pattern元素,count指定最多只遍历count个元素。
2.6,ZSet相关命令
【ZSet相关命令】ZSet相当于Set的增强版,它会为每个元素都分配一个double类型的分数(score),并按该score对集合中元素进行排序。每个元素在Zset中都有一个对应的索引,用于支持高效的插入、删除和查找操作。
Zset的索引采用了跳表的数据结构,因为跳表在插入、删除和查找操作中具有较高的效率和较小的时间复杂度。跳表是一种基于链表的数据结构,可以支持快速的查找、插入和删除操作,其时间复杂度与平衡树相当,但实现相对简单。跳表的特点是在每个节点上增加了多级索引,这些索引可以跨越多个节点,从而加速查找操作的速度。
通过使用跳表作为Zset的索引数据结构,Redis可以在log(N)的时间复杂度内完成插入、删除和查找操作,而不需要对整个集合进行遍历,从而保证了高效的性能和低延迟。
【ZSet和List的异同】
- 二者都是有序的。
- 二者都可以获得某一范围的元素。
- 列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。
- 有序集合类型是使用散列表和跳跃表(Skip list)实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度是O(log(N)))。
- 列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)。
- 有序集合要比列表类型更耗费内存。
【常用方法】
- zadd key score member[score member...]:向ZSet中添加一个或多个元素,或者更新已有元素的score。
- zcard key:返回key对应的ZSet中元素的个数。
- zcount key min max:返回ZSet中score位于min和max之间的元素个数。
- zdiff numkeys key [key...][WITHSCORES]:计算给定ZSet之间的差值。该命令在Redis6.2及更新版本中才使用。
- zdiffstore destination numkeys key[key...]:zdiff的存储版本,将给定ZSet之间的差值保存到destination中。该命令在Redis6.2及更新版本中才可使用。
- zincrby key increment member:将member元素的score增加到increment。
- zinter numkeys key[key...]:计算给定ZSet的交集。该命令在Redis6.2及更新版本才可用。
- zinterstore destination numkeys key[key...]:zinter的存储版本,将给定zset的交集保存到destination中。交集中元素的score是相同元素的score之和。
- zlexcount key min max:返回ZSet中按字典排序时从min到max之间所有元素的个数。
当向ZSet中添加多个score相等的元素时,ZSet就会使用字典顺序对这些元素进行排序,此时就可按字典顺序来获取指定范围内元素的个数。
- zpopmax key[count]:弹出ZSet中score最大的元素。
- bzpopmax key[key...]:zpopmax的阻塞版本。该命令会阻塞进程,直到指定ZSet有元素弹出或超时。
- zrange key start stop[WITHSCORES]:返回ZSet中从start索引到stop索引范围内的元素(及score)。索引支持负数,负数表示从最后面开始。
- zrangebylex key min max[LIMIT offset count]:返回ZSet中按字典排序时从min到max之间所有元素。
- zrangebyscore key min max[WITHSCORES][LIMIT offset count]:返回ZSet中score位于min和mx之间的所有元素。
- zrank key member:返回ZSet中指定元素的索引。score最小的元素的索引是0。
- zrem key member [member...]:删除Zset中一个或多个元素。
- zremrangebylex key min max:删除ZSet中按字典排序时从min到max之间的所有元素。
- zremrangebyrank key start stop:删除ZSet中从start索引到stop索引之间的所有元素。
- zremrangebyscore key min max:删除ZSet中score位于min和max之间的所有元素。
- zscore key member:获取指定元素的score。
- zunion numberkeys key[key...]:计算给定ZSet的并集。
- zunionstore destination numkeys key[key...]:zunion的存储版本,将给定ZSet的并集保存到destination中。
- zmscore key member[member...]:获取多个元素的score。
- zscan key cursor[MATCH pattern][COUNT count]:使用cursor遍历key对应的ZSet。pattern指定只遍历匹配pattern的元素,count指定最多只遍历count个元素。
2.7,Hash相关命令
【Hash相关命令】Hash类型是一个key和value都是String类型的key-value对。Hash类型适合存储对象。每个Hash最多可存储 个key-value对。
- hdel key field[field...]:删除Hash对象中一个或多个key-value对。此处的field参数其实代表Hash对象中的key,后面提到的field参数皆是如此。
- hexists key field:判断Hash对象中指定key是否存在。
- hget key field:获取Hash对象中指定key对应的value。
- hgetall key:获取Hash对象中所有的key-value对。
- hincrby key field increment:为Hash对象中指定的key增加increment。
- hincrbyfloat key field increment:hincrby的浮点数版本,支持小数。
- hkeys key:获取Hash对象中所有的key。
- hlen key:获取Hash对象中key-value对的数量。
- hmget key field[field...]:hget的加强版,可同时获取多个key对应的value。
- hset key field value:为hash对象设置一个key-value对。如果field对应的key已经存在,新设置的value将会覆盖原有的value。
- hmset key field value[field value...]:hset的加强版,可同时设置多个key-value对。
- hsetnx key field value:hset的安全版本,只有当field对应的key不存在时,才能设置成功。
- hstrlen key field:获取hash对象中指定key对应的value的字符串长度。
- hvals key:获取hash对象中所有的value。
- hscan key cursor [MATCH pattern][COUNT count]:遍历hash对象。
2.8,事务相关命令&发布/订阅相关命令
【事务相关命令】Redis事务保证事务内的多条命令按顺序作为整体执行,其他客户端发出请求绝不可能被插入到事务处理的中间,这样可以保证事务内所有命令作为一个隔离操作被执行。Redis事务同样具有原子性,事务内所有的命令要么全部被执行,要么全部被抛弃。例如Redis在事务执行过程中遇到数据库宕机,假如事务已经执行了一半命令,Redis将会自动回滚这些已经执行过的命令。
- discard:取消事务,放弃执行事务块内的所有命令。
- exec:执行事务。
- multi:开启事务。
- watch key[key...]:监视一个或多个key,如果在事务执行之前这些key对应的值被其他命令改动,事务会自动中断。
- unwatch:取消watch命令对所有key的监视。
redis> MULTI OK redis> SADD "user:1:following" 2 QUEUED redis> SADD "user:2:followers" 1 QUEUED redis> EXEC 1) (integer) 1 2) (integer) 1
Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则 Redis 会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
【发布/订阅相关命令】Redis内置支持发布/订阅的消息机制,消息订阅者可以订阅一个或多个channel,每个channel也可被多个消息订阅者订阅。只要消息发布者向某个channel发布消息,该消息就会同时被该channel的多个消息订阅者收到。
- subscribe channel[channel ...]:订阅一个或多个channel。
- unsubscribe[channel[channel...]]:取消订阅一个或多个channel,如果不带参数,则表明取消订阅所有channel。
- psubscribe pattern[pattern...]:按模式匹配的方式订阅一个或多个channel。
- punsubscribe[pattern[pattern...]]:按模式匹配的方式取消订阅一个或多个channel,如果不带参数,则表明取消订阅所有channel。
- publish channel message:向指定channel发布消息。
- pubsub subcommand[argument[argument...]]:检查订阅/发布系统的状态。
3,持久化,集群,缓存问题
3.1,持久化
Redis的强劲性能很大程度上是由于其将所有数据都存储在了内存中,然而当Redis重启后,所有存储在内存中的数据就会丢失。在一些情况下,我们会希望 Redis 在重启后能够保证数据不丢失,例如:
- 将Redis作为数据库使用时。
- 将 Redis 作为缓存服务器,但缓存被穿透后会对性能造成较大影响,所有缓存同时失效会导致缓存雪崩,从而使服务无法响应。
这时候需要 Redis 能将数据从内存中以某种形式同步到硬盘中,使得重启后可以根据硬盘中的记录恢复数据。这一过程就是持久化。
- RDB:根据指定的规则“定时”将内存中的数据存储在硬盘上。
- AOF:在每次执行命令后将命令本身记录下来。
两种持久化方式可以单独使用其中一种,但更多情况下是将二者结合使用。Redis 允许同时开启 AOF 和 RDB,既保证了数据安全又使得进行备份等操作十分容易。此时重新启动Redis后Redis会使用AOF文件来恢复数据,因为AOF方式的持久化可能丢失的数据更少。
- 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
- 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
- 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
- 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。
【RDB】RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的所有数据生成一份副本并存储在硬盘上,这个过程即为“快照”。Redis会在以下几种情况下对数据进行快照:
根据配置规则进行自动快照:Redis允许用户自定义快照条件,当符合快照条件时,Redis会自动执行快照操作。进行快照的条件可以由用户在配置文件中自定义,由两个参数构成:时间窗口M和改动的键的个数N。每当时间M内被更改的键的个数大于N时,即符合自动快照条件。
用户执行 SAVE或 BGSAVE命令。
执行 FLUSHALL命令:当执行 FLUSHALL 命令时,Redis 会清除数据库中的所有数据。需要注意的是,不论清空数据库的过程是否触发了自动快照条件,只要自动快照条件不为空,Redis就会执行一次快照操作。
执行复制(replication)时:当设置了主从模式时,Redis 会在复制初始化时进行自动快照。关于主从模式和复制的过程会在第8章详细介绍,这里只需要了解当使用复制操作时,即使没有定义自动快照条件,并且没有手动执行过快照操作,也会生成RDB快照文件。
【优点】
- 只有一个文件 dump.rdb,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以 是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操 作, 保证了 redis 的高性能)
- 相对于数据集大时,比 AOF 的启动效率更高。
【缺点】
- 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
【AOF】当使用Redis存储非临时数据时,一般需要打开AOF持久化来降低进程中止导致的数据丢失。AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis 的性能,但是大部分情况下这个影响是可以接受的,另外使用较快的硬盘可以提高AOF的性能。
虽然每次执行更改数据库内容的操作时,AOF都会将命令记录在AOF文件中,但是事实上,由于操作系统的缓存机制,数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。在默认情况下系统每30秒会执行一次同步操作,以便将硬盘缓存中的内容真正地写入硬盘,在这30秒的过程中如果系统异常退出则会导致硬盘缓存中的数据丢失。一般来讲启用AOF持久化的应用都无法容忍这样的损失,这就需要Redis在写入AOF文件后主动要求系统将缓存内容同步到硬盘中。
【优点】
- 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行 一次命令操作就记录到 aof 文件中一次。
- 通过 append 模式写文件,即使中途服务器宕机,可以通过 redischeck-aof 工具解决数据一致性问题。
- AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会 对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。
【缺点】
- AOF 文件比 RDB 文件大,且恢复速度慢。
- 数据集大的时候,比 rdb 启动效率低。
3.2,集群(同步策略)
小型项目使用一台 Redis 服务器已经非常足够了,然而现实中的项目通常需要若干台Redis服务器的支持:
从结构上,单个 Redis 服务器会发生单点故障,同时一台服务器需要承受所有的请求负载。这就需要为数据生成多个副本并分配在不同的服务器上。
从容量上,单个 Redis 服务器的内存非常容易成为存储瓶颈,所以需要进行数据分片。
同时拥有多个 Redis 服务器后就会面临如何管理集群的问题,包括如何增加节点、故障恢复等操作。
- 主从模式:一个主数据库带多个从数据库,主数据库负责接受客户端读/写数据,并将数据自动同步到各从数据库中作为备份。任何从数据库宕机,完全不影响客户端的使用,只是少一个备份而已;如果主数据库宕机,从数据库只能接受客户端读取数据,不再接受写入数据。只有在主数据库重新启动之后,Redis才能重新接受客户端写入数据。
- 哨兵模式:就是在主从模式上增加哨兵进程,哨兵进程不与客户端交互,它只负责监控所有的主、从数据库,如果主数据库宕机,它会挑选一个从数据库,并修改它的配置文件,使之成为新的主数据库,然后修改其他从数据库的配置文件,让它们从属于新的主数据库;如果原来的主数据库重新启动,那么它的配置文件也会被修改;它会变成从数据库,并从属于新的主数据库。
- Cluster模式:主要是对哨兵模式的进一步扩展,这种模式支持多个网络节点,从而允许Redis将不同数据存储到不同的节点中。Cluster模式主要是为了解决单机Redis容量有限的问题,将大量数据按一定的规则分配到不同的Redis节点中,每个Redis节点都以哨兵模式运行。
【主从模式】数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
在 Redis 中使用复制功能非常容易,只需要在从数据库的配置文件中加入“slaveof 主数据库地址主数据库端口”即可,主数据库无需进行任何配置。
【问题】如何保证主从一致性
【答案】
- 使用 Redis Sentinel 或 Redis Cluster 等高可用方案,以避免单点故障导致数据丢失或不一致。
- 在从节点上启用 AOF 持久化,以确保即使主节点挂掉,从节点也能够在重启后重新同步数据。
- 监控系统日志和网络状况,及时发现并解决主从数据不一致的问题。
【哨兵】哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个:
- 监控主数据库和从数据库是否正常运行。
- 主数据库出现故障时自动将从数据库转换为主数据库。
在一个一主多从的Redis系统中,可以使用多个哨兵进行监控任务以保证系统足够稳健。哨兵将从停止服务的主数据库的从数据库中挑选一个来充当新的主数据库。挑选的依据如下:
- 所有在线的从数据库中,选择优先级最高的从数据库。优先级可以通过slave-priority选项来设置。
- 如果有多个最高优先级的从数据库,则复制的命令偏移量(见8.1.7节)越大(即复制越完整)越优先。
- 如果以上条件都一样,则选择运行ID较小的从数据库。
【问题】Redis中哨兵如何监控Redis系统的运行状态?
【答案】Redis中哨兵可以通过向被监控的Redis节点发送命令(如
PING
)来监测其是否存活,同时还可以通过向被监控节点的INFO
命令获取其运行状态信息,例如主从复制状态、内存使用情况、连接数、已执行命令数等。哨兵会定期(默认10秒钟)向Redis节点发送INFO
命令,以确保节点的运行状态正常。如果哨兵在指定时间内没有收到被监控节点的响应,则会将其标记为下线状态并启动故障转移操作。
【集群】即使使用哨兵,此时的 Redis 集群的每个数据库依然存有集群中的所有数据,从而导致集群的总数据存储量受限于可用存储内存最小的数据库节点,形成木桶效应。由于Redis中的所有数据都是基于内存存储,这一问题就尤为突出了,尤其是当使用 Redis 做持久化存储服务使用时。
对 Redis 进行水平扩容,在旧版 Redis 中通常使用客户端分片来解决这个问题,即启动多个 Redis 数据库节点,由客户端决定每个键交由哪个数据库节点存储,下次客户端读取该键时直接到该节点读取。这样可以实现将整个数据分布存储在N个数据库节点中,每个节点只存放总数据量的 1/N。但对于需要扩容的场景来说,在客户端分片后,如果想增加更多的节点,就需要对数据进行手工迁移,同时在迁移的过程中为了保证数据的一致性,还需要将集群暂时下线,相对比较复杂。
无论如何,客户端分片终归是有非常多的缺点,比如维护成本高,增加、移除节点较繁琐等。Redis 3.0版的一大特性就是支持集群(Cluster)功能。集群的特点在于拥有和单机实例同样的性能,同时在网络分区后能够提供一定的可访问性以及对主数据库故障恢复的支持。另外集群支持几乎所有的单机实例支持的命令,对于涉及多键的命令(如MGET),如果每个键都位于同一个节点中,则可以正常支持,否则会提示错误。除此之外集群还有一个限制是只能使用默认的0号数据库,如果执行SELECT切换数据库则会提示错误。
【哨兵和集群】哨兵与集群是两个独立的功能,但从特性来看哨兵可以视为集群的子集,当不需要数据分片或者已经在客户端进行分片的场景下哨兵就足够使用了,但如果需要进行水平扩容,则集群是一个非常好的选择。
【问题】Redis的集群的桶限制?
【答案】Redis集群中,每个节点都有一个哈希槽(hash slot)的概念,哈希槽是一个整数,范围是0-16383。当一个key需要被存储到Redis集群中时,Redis会使用CRC16算法计算出该key的哈希值,然后将哈希值对16384取模,得到的结果就是该key所属的哈希槽。
Redis集群中,每个节点负责一部分哈希槽,节点之间通过Gossip协议通信,共同维护集群的状态。当一个节点加入或离开集群时,集群会自动进行哈希槽的重新分配,以保证每个节点负责的哈希槽数量尽可能均衡。
在Redis集群中,每个节点都有一个桶限制的概念,桶限制指的是每个节点最多可以存储多少个key。当一个节点存储的key数量超过了桶限制时,节点就会进入写保护模式,禁止对该节点进行写操作,直到节点的key数量降到桶限制以下。桶限制的默认值是1万个key。
3.3,缓存延迟,缓存穿透,缓存雪崩,缓存击穿,缓存污染
【缓存延迟】当Redis缓存中存在大量热点数据并且访问速度变慢时,可以采取以下方法来解决问题:
增加内存容量:如果可能的话,可以考虑增加Redis服务器的内存容量,以容纳更多的数据。较大的内存容量可以减少内存淘汰(eviction)的频率,从而提高访问速度。
LRU算法优化:Redis默认使用Least Recently Used (LRU)算法来选择要删除的缓存数据,当内存不足时。可以考虑调整LRU算法的参数,或者使用其他淘汰策略,如LFU(Least Frequently Used)或随机淘汰策略,以更好地满足热点数据的需求。
数据分片:将热点数据分散到多个Redis实例或分片中。这可以通过使用Redis Cluster或者分布式缓存方案来实现。每个分片只负责部分热点数据,从而减轻单个Redis实例的负担,提高访问速度。
使用持久化:如果热点数据量较大,可以考虑使用Redis的持久化机制,将部分数据存储到磁盘中,以释放内存。需要时,可以从磁盘中加载数据到内存。不过要注意,持久化操作可能会导致短暂的性能下降,因此需要权衡。
使用缓存预热:在应用启动时,可以通过扫描数据库或者其他数据源来预热Redis缓存,将热点数据加载到缓存中,以减少冷启动时的访问延迟。
缓存降级:对于极其热点的数据,可以考虑将其放在本地内存中,而不是Redis中。这样可以提高访问速度,但需要牺牲一些Redis的特性,如持久化和集群支持。
使用分布式缓存:考虑使用分布式缓存系统,如Memcached或使用云服务提供的缓存服务,以扩展缓存的容量和性能。
优化查询和缓存策略:优化应用程序的查询和缓存策略,确保只有真正需要缓存的数据被缓存,避免不必要的缓存操作。
监控和性能调优:定期监控Redis的性能和资源使用情况,识别瓶颈并进行性能调优。使用Redis的性能分析工具来诊断问题并采取相应的措施。
【缓存穿透】缓存穿透是指当请求一个不存在的key时,缓存层无法命中,导致请求直接穿透到后端存储系统,增加了后端存储系统的负担,甚至可能导致系统瘫痪。解决缓存穿透的方法如下:
- 使用布隆过滤器:在缓存层和后端存储系统之间添加一个布隆过滤器,可以判断请求的key是否存在。如果布隆过滤器判断key不存在,则可以直接返回结果,避免请求直接穿透到后端存储系统。
- 设置空值缓存:在缓存层中设置空值缓存,即将请求不存在的key的结果缓存为空,避免对后端存储系统的重复查询。
布隆过滤器的原理:当一个元素被加入集合时,通过K个哈希函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过哈希函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。
【缓存雪崩】缓存雪崩是指缓存层中的大量key同时失效,导致请求直接穿透到后端存储系统,造成系统瘫痪。解决缓存雪崩的方法如下:
- 设置过期时间随机化:在设置缓存的过期时间时,可以将过期时间随机化,避免大量的key同时失效。
- 设置热点数据永不过期:将一些热点数据设置为永不过期,避免这些数据同时失效。
- 使用分布式锁:在缓存层中使用分布式锁,避免多个请求同时访问后端存储系统。
【缓存击穿】缓存击穿是指当一个热点key失效时,大量请求同时访问该key,导致请求直接穿透到后端存储系统,造成系统瘫痪。解决缓存击穿的方法如下:
- 设置热点数据永不过期:将热点数据设置为永不过期,避免热点key失效。
- 使用互斥锁:在缓存层中使用互斥锁,避免多个请求同时访问后端存储系统。
- 布隆过滤器:数据结构来快速检测某个请求是否合法,如果请求数据非法,可以直接拒绝。
- 设置空值缓存:在缓存层中设置空值缓存,即将请求不存在的key的结果缓存为空,避免对后端存储系统的重复查询。
- 限流:使用限流策略,限制访问缓存或后端服务的请求速率,以减轻数据库或服务的压力。
- 失败缓存:当数据库查询失败时,可以将失败结果缓存一段时间,防止连续的相同失败请求访问数据库。
【缓存污染】缓存污染是指缓存层中缓存的数据被篡改,导致后续请求获取到错误的数据,甚至可能导致安全漏洞。解决Redis缓存污染的方法如下:
- 序列化和反序列化:在将数据存储到Redis中时,需要对数据进行序列化和反序列化,以便存储和读取。序列化和反序列化过程中,需要对数据进行校验和过滤,以避免恶意数据的插入。
- 使用Hash:在Redis中,可以使用Hash结构来存储缓存数据,这样可以将数据分散到不同的Hash槽中,避免缓存数据的相互干扰。同时,在存储Hash结构时,需要对每个字段进行校验和过滤,以避免恶意数据的插入。
- 使用Redis ACL:Redis 6.0及以上版本提供了访问控制列表(ACL)功能,可以限制对Redis服务器的访问,以保护Redis服务器的安全性。通过设置ACL规则,可以限制某些用户对Redis服务器的访问,避免恶意用户篡改缓存数据。
- 定期刷新缓存:定期刷新缓存可以避免缓存数据长时间未更新,从而导致缓存数据被篡改的风险。可以设置一个定时任务,定期从后端存储系统中获取最新的数据,并更新缓存中的数据。
- 加密缓存数据:对缓存数据进行加密可以保护缓存数据的安全性,防止恶意用户篡改缓存数据。在存储缓存数据时,可以使用对称加密或非对称加密算法对数据进行加密,然后存储加密后的数据到Redis中。在读取缓存数据时,需要进行解密操作,以获取原始数据。
4,Lettuce
4.1,基本概念
- Lettuce:可伸缩的Redis客户端,基于Netty NIO框架来有效地管理多个连接。
- Spring Data Redis:Spring Data项目中的模块,封装了多个Redis客户端,让开发者对Redis的操作更加高效便捷。
- Spring Data:Spring框架中的重要组成部分,它极大地简化了构建基于Spring框架应用的数据操作,包括非关系数据库、Map-Reduce框架、云数据服务等,同时也支持关系数据库。
- spring-boot-starter-data-redis:Spring Boot提供的Redis集成启动器(Starter),依赖于spring-data-redis和lettuce库。
Redis客户端有很多选项可供选择,在Java领域中,目前使用最广泛的Redis客户端有Lettuce、Jedis等,Spring Data Redis模块默认使用Lettuce。Jedis在实现上直连Redis服务器,在多线程环境下是非线程安全的,除非使用连接池为每个Jedis实例增加物理连接。Lettuce基于Netty的连接实例(StatefulRedisConnection)可以在多个线程间并发访问,并且是线程安全的,它支持多线程环境下的并发访问,同时也是可伸缩的设计,在一个连接实例不够的情况下可以按需增加连接实例。
这些底层客户端的功能相似,都提供了一个API来执行Redis命令。SpringData Redis底层使用的是Lettuce,主要有4个核心API:
- RedisURI:用于封装Redis服务器的连接信息。
- RedisClient:代表Redis客户端,如果连接Cluster模式的Redis,则使用RedisClusterClient。
- StatefulConnection<K,V>:代表Redis连接的父接口,它派生了不少子接口来代表不同的连接。
- RedisCommands:用于执行Redis命令的接口,它的方法几乎覆盖了Redis的所有命令,它的方法名和Redis命令名一一对应的。
在实际开发中,RedisCommands是使用最多的API,它的功能实际上就相当于redis-cli。
RedisURI就是用于封装服务器的连接信息的,如服务器地址、数据库、密码等。Lettuce提供了三种方式来构建RedisURI:
- 调用create()静态方法来构建RedisURI:
RedisURI.create("redis://localhost/");
- (推荐)调用Builder来构建RedisURI:
RedisURI.Builder.redis("localhost",6379) .withPassword("password") .withDatabase(1) .build();
- (不推荐,还需要调用setter设置其他属性)调用构造器来构建RedisURI:
new RedisURI("localhost",6379,Duration.ofSeconda(60));
有了RedisURI之后,接下来以RedisURI为参数,调用RedisClient(或RedisClusterClient)的create()静态方法即可创建RedisClient(或RedisClusterClient)对象。
有了RedisClient或RedisClusterClient对象之后,根据Redis的运行模式调用对应的connectXxx()方法来获取StatefulConnection对象。
- StatefulRedisConnection:最基本的Redis连接。
- StatefulRedisPubSubConnection:带消息发布/订阅功能的Redis连接。
- StatefulRedisMasterSlaveConnection:主从模式的Redis连接。
- StatefulRedisSentinelConnection:哨兵模式的Redis连接。
有了StatefulRedisXxxConnection连接对象之后,调用它的如下3个方法来创建RedisXxxCommands对象。
- sync():创建同步模式的RedisCommands对象。
- async():创建异步模式的RedisAsyncCommands对象。
- reactive():创建反应式模式的RedisReactiveCommands。
RedisCommands的作用类似于redis-cli工具,可用于执行各种Redis命令。其中:
RedisAsynCommands是异步版本,而RedisReactiveCommands则是反应式版本。
RedisCommands还有一个RedisPubSubAsyncCommand子接口,用于支持消息发布/订阅功能;RedisAsyncCommands有一个RedisPubSubAsyncCommands子接口,用于支持消息发布/订阅功能;RedisReactiveCommands有一个RedisPubSubReactiveCommands子接口,用于支持消息发布/订阅功能。
4.3,同步方式
Lettuce操作Redis数据库的步骤:
- 定义RedisURI,再以RedisURI为参数,创建RedisClient或RedisClusterClient对象。
- 调用RedisClient或RedisClusterClient的connectXxx()方法连接Redis服务器,根据所连接的Redis服务器的状态不同,该方法返回StatefulRedisXxxConnection连接对象。
- 调用连接对象的sync()、async()或reactive()方法创建同步、异步或反应式模式的RedisCommands对象。
- 调用RedisCommands执行Redis命令。这一步是变最大的,因为RedisCommands可以执行Redis的全部命令。
- 关闭资源。关闭资源时按照惯例“先开后闭”,因此线关闭与Redis的连接对象,再关闭RedisClient对象。
<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.0.1.RELEASE</version> </dependency>
import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.SetArgs; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Map; public class SyncTest{ static RedisClient redisClient; static StatefulRedisConnection<String, String> conn; // 初始化RedisClient、StatefulRedisConnection的方法 public static void init(){ // 创建RedisURI对象 RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPassword(new char[]{'3', '2', '1', '4', '7'}) .withDatabase(0) .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); // 创建RedisClient redisClient = RedisClient.create(redisUri); // 获取StatefulRedisConnection conn = redisClient.connect(); } public static void closeResource(){ // 关闭StatefulRedisConnection conn.close(); // 关闭RedisClient redisClient.shutdown(); } public static void main(String[] args){ init(); // 创建StatefulRedisConnection StatefulRedisConnection<String, String> conn = redisClient.connect(); // 创建同步模式的RedisCommands RedisCommands<String, String> redisCommands = conn.sync(); // SetArgs,主要用于设置超时时长 SetArgs setArgs = SetArgs.Builder.nx().ex(2000); // 执行PING命令 System.out.println(redisCommands.ping()); // 执行SET命令 String result1 = redisCommands.set("name", "燕双嘤", setArgs); System.out.println(result1); // 执行HSET命令 Long result2 = redisCommands.hset("user", Map.of("name", "杜马", "age", "23", "address", "光复社")); System.out.println(result2); // 执行GET命令 String result3 = redisCommands.get("name"); System.out.println(result3); // 执行HGET命令 System.out.println(redisCommands.hget("user", "address")); closeResource(); // 关闭资源 } }
4.4,异步方式
异步方式来操作Redis数据库:
- 调用async()方法来创建RedisAsyncCommands对象。
- RedisAsyncCommands执行Redis命令后返回值是RedisFuture,它包装了实际的返回值。RedisFuture继承了JDK并发编程的CompletionStage和Future,因此可通过异步方式来获取数据。
public class Lettuce_Test { ... public static void main(String[] args) throws InterruptedException { init(); //创建异步模式的RedisAsyncCommands RedisAsyncCommands<String,String> redisCommands = conn.async(); //执行Ping命令 RedisFuture<String> result = redisCommands.ping(); result.thenAccept(System.out::println); //执行lpush命令 redisCommands.lpush("book","Java","C++","C#").thenAccept(System.out::println); //执行Sadd命令 redisCommands.sadd("name","杜马","燕双嘤"); //执行Srandmember命令 redisCommands.srandmember("name").thenAccept(System.out::println); Thread.sleep(500); closeResource(); } }
使用异步方式执行Redis命令时,程序调用async()方法来创建RedisAsyncCommands对象。之后以异步方式执行Redis命令的返回值:RedisFuture,因此程序使用theAccept()方法来获取Redis命令实际返回的数据。
由于thenAccept()方法会以异步方式来获取Redis命令实际返回的数据,它不会阻塞主线程,因此可能出现的情况是:程序主线程执行完成(程序退出)后,Redis命令的结果还没有获取到,故上面程序在关闭资源之前让主线程暂停0.5s,保证程序退出之前能获取到Redis命令的执行结果。
4.4,反应式方式
反应式方式来操作Redis数据库:
- 调用reactive()方法来创建RedisReactiveCommands对象。
- RedisReactiveCommands执行Redis命令后的返回值是Mono或Flux,它包装了实际的返回值,它们就是反应式API因此可通过反应式方式来获取数据。通过这两个类型,开发人员可以利用响应式流式处理方式进行数据访问,以提高应用程序的响应能力和性能,同时还可以避免在繁忙时阻塞线程。
public class Lettuce_Test { ... public static void main(String[] args) throws InterruptedException { init(); //创建反应式模式RedisReactiveCommands RedisReactiveCommands<String,String> redisCommands = conn.reactive(); //执行Ping命令 Mono<String> result = redisCommands.ping(); result.subscribe(System.out::println); //执行zadd命令 redisCommands.zadd("key1",0.3,"Java",0.4,"C++",0.5,"Python").subscribe(System.out::println); //执行zrank命令 redisCommands.zrank("key1","ysy").subscribe(System.out::println); //执行zrange命令 redisCommands.zrange("key1",1,2).subscribe(System.out::println); //执行zpopmax命令 redisCommands.zpopmax("key1").subscribe(System.out::println); Thread.sleep(500); closeResource(); } }
使用反应式方式执行Redis命令时,程序调用reactivae()方法来创建RedisReactiveCommands对象。反应式方式执行Redis命令的返回值:Mono,因此程序使用subscribe()方法来订阅反应式源中的数据——这些数据来自Redis命令的返回值。
4.5,连接池(消息订阅者方式)
从Redis 6.0开始,Redis支持使用多线程来接收、处理客户端命令,因此应用程序可使用连接池来管理Redis连接。
使用 Lettuce连接池需要Apache Commons Pool2的支持,因此在 pom.xml文件中添加如下依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.9.0</version> </dependency>
消息订阅者:
import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.pubsub.RedisPubSubAdapter; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands; import io.lettuce.core.support.ConnectionPoolSupport; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import java.time.Duration; import java.time.temporal.ChronoUnit; public class Subscriber{ static RedisClient redisClient; static StatefulRedisConnection<String, String> conn; // 定义连接池 static GenericObjectPool<StatefulRedisPubSubConnection<String, String>> pool; // 初始化RedisClient、StatefulRedisConnection和连接池的方法 public static void init(){ // 创建RedisURI对象 RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPassword(new char[]{'3', '2', '1', '4', '7'}) .withDatabase(0) .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); // 创建RedisClient redisClient = RedisClient.create(redisUri); // 获取StatefulRedisConnection conn = redisClient.connect(); var conf = new GenericObjectPoolConfig<StatefulRedisPubSubConnection <String, String>>(); conf.setMaxTotal(20); // 设置允许的最大连接数 // 创建连接池对象(其中连接由redisClient的connectPubSub方法创建) pool = ConnectionPoolSupport.createGenericObjectPool( redisClient::connectPubSub, conf); } public static void closeResource(){ // 关闭连接池 pool.close(); // 关闭StatefulRedisConnection conn.close(); // 关闭RedisClient redisClient.shutdown(); } public static void main(String[] args) throws Exception{ init(); RedisCommands<String, String> redisCommands = conn.sync(); // 设置redisCommands只通知key过期(Ex)的事件 redisCommands.configSet("notify-keyspace-events", "Ex"); // 设置该key-value于3秒之后过期,该过期消息会被订阅者收到 redisCommands.setex("organization", 3, "NUDT"); // 通过连接池获取连接 var subConn = pool.borrowObject(); subConn.addListener(new RedisPubSubAdapter<>(){ @Override // 订阅channel成功时触发该方法 public void subscribed(String channel, long count){ System.out.println("订阅成功,channel: " + channel + ", count:" + count); } // 收到channel的消息时触发该方法 @Override public void message(String channel, String message){ System.out.println("channel:" + channel + ", message: " + message); } // 订阅pattern的channel成功时触发该方法 @Override public void psubscribed(String pattern, long count){ System.out.println("订阅成功,pattern: " + pattern + ", count:" + count); } // 收到pattern的channel的消息时触发该方法 @Override public void message(String pattern, String channel, String message){ System.out.println("pattern:" + pattern + ", channel:" + channel + ", message: " + message); } }); RedisPubSubCommands<String, String> subCommands = subConn.sync(); // 订阅pattern模式的channel subCommands.psubscribe("__keyevent@0__:expired"); // 订阅channel subCommands.subscribe("mychannel"); Thread.sleep(50000); closeResource(); } }
消息发布者:
package org.crazyit.app; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands; import io.lettuce.core.support.ConnectionPoolSupport; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import java.time.Duration; import java.time.temporal.ChronoUnit; public class Publisher{ static RedisClient redisClient; // 定义连接池 static GenericObjectPool<StatefulRedisPubSubConnection<String, String>> pool; // 初始化RedisClient、连接池的方法 public static void init(){ // 创建RedisURI对象 RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPassword(new char[]{'3', '2', '1', '4', '7'}) .withDatabase(0) .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); // 创建RedisClient redisClient = RedisClient.create(redisUri); var conf = new GenericObjectPoolConfig<StatefulRedisPubSubConnection <String, String>>(); conf.setMaxTotal(20); // 设置允许的最大连接数 // 创建连接池对象(其中连接由redisClient的connectPubSub方法创建) pool = ConnectionPoolSupport.createGenericObjectPool( redisClient::connectPubSub, conf); } public static void closeResource(){ // 关闭连接池 pool.close(); // 关闭RedisClient redisClient.shutdown(); } public static void main(String[] args) throws Exception{ init(); // 调用borrowObject()方法获取连接 RedisPubSubCommands<String, String> pubCommands = pool.borrowObject().sync(); pubCommands.publish("mychannel", "I want to learn Redis"); closeResource(); } }
上面是同步模式下的消息订阅/发布,Lettuce当然也支持异步模式、反应式模式的消息订阅/发布。
- 异步模式的消息订阅/发布:调用StatefulRedisPubSubConnection的async()方法即可,该方法返回的是RedisPubSubAsyncCommands,该返回值会以异步方式执行消息订阅/发布。
- 反应式模式的消息订阅/发布:调用StatefulRedisPubSubConnection的reactive()方法即可,该方法返回的是RedisPubSubReactiveCommands,该返回值会以反应式方式执行消息订阅/发布。
5,RedisTemplate
5.1,基本概念
SpringBoot为支持Redis提供了spring-boot-start-data-redis.jar,该Starter使用Spring Data Redis对底层Lettuce(或Jedis)进行了封装,并为Lettuce(或Jedis)提供了自动配置,且该Starter默认以Lettuce作为Redis客户端,除非显示指定使用Jedis客户端。使用后它可以通过序列化把Java对象转换,使得Redis能把它存储起来,并在读取的时候,再把序列化过的字符串转化为Java对象,这样在Java环境中使用Redis就更加简单了。
SpringBoot会为Redis自动配置RedisConnectionFactory、StringRedisTemplate、ReactiveStringRedisTemplate(反应式API),因此可将它们注入任意其他组件(如DAO组件)。
配置:在默认情况下,RedisConnectionFactory自动连接位于localhost:6379的Redis服务器,如果需要改变默认位置,则在application.properties中可通过spring.redis开头的属性进行配置。
spring.redis.host=192.168.1.188 spring.redis.port=6380
如果在Spring容器中配置自己的RedisConnectionFactory Bean,SpringBoot就不会自动配置RedisConnectionFactory。但RedisTemplate可在容器中额外配置很多个,只要额外配置的RedisTemplate的id不是redisTemplate,SpringBoot就依然会自动配置一个id为redisTemplate的RedisTemplate。如果SpringBoot在类加载路径下找到Apache Commons Pool2依赖库,SpringBoot会自动池化连接工厂。
Redis连接池相关信息,可以通过以“spring.redis.lettuce.pool”开头的属性进行配置:
#最大活动连接数 spring.redis.lettuce.pool.max-active=20 #最大空闲连接数 spring.redis.lettuce.pool.max-idle=20 #最小空闲连接数 spring.redis.lettuce.pool.min-idle=2
有了RedisTemplate(实际上是StringRedisTemplate)之后,接下来可调用它的如下方法:
// 操作String类型 redisTemplate.opsForValue().set("key","value"); // 操作Hash类型 redisTemplate.opsForHash().put("hash","test","hello"); // 操作List redisTemplate.opsForList().leftPush("list","weiz"); // 操作Set redisTemplate.opsForSet().add("set","weiz") // 操作ZSet redisTemplate.opsForZSet().add("zset","weiz");
RedisTemplate与RedisCommands的区别:RedisCommands的做法是它自己为Redis所有命令定义了对应的方法;而RedisTemplate则不同,它对Redis命令进行了分类,不同的命令由不同的接口提供支持——比如操作List的命令,由ListOperations负责提供;操作Set的命令由SetOperation负责提供。
如果要执行更细致的操作,RedisTemplate则提供了一系列execute()方法,这些方法都需要传入一个Lambda形式(或匿名内部类形式)的Callback对象,开发者在实现Callback接口中的抽象方法时,可访问到RedisConnection等底层API,从而直接使用RedisConnection等底层API来操作Redis数据库。此外,RedisTemplate还提供了一些直接操作key的方法。
BoundValueOperations:RedisTemplate提供了API用于对key执行bound(绑定)便捷化操作,可以通过bound封装指定的key,然后执行一系列的操作,而无须显式地再次指定key,即BoundKeyOperations将事务操作封装,由容器控制。
- BoundValueOperations是针对String类型的绑定操作。
- BoundSetOperations是针对Set类型的绑定操作。
- BoundListOperations是针对List类型的绑定操作。
- BoundZSetOperations是针对ZSet类型的绑定操作。
- BoundHashOperations是针对Hash类型的绑定操作。
例如,我们在某个类或方法中需要反复操作某个特定的key中的数据,则可以先定义对应的BoundKeyOperations,然后使用此类重复操作key中的数据,无须再调用方法中指定的key。
String key = "weiz"; // 获取Redis对value的操作对象,需要先设置key BoundValueOperations boundTemplate = redisTemplate.boundValueOps(key); boundTemplate.set("bound test"); // 获取value String value = boundTemplate.get();
通过上面的示例,首先定义key为“weiz”的BoundValueOperations实例,然后在后续的操作中直接使用定义的boundTemplate实例,操作这个key对应的数据,无须在调用方法中指定key。
5.2,RedisTemplate使用
【简单使用】
- 引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 引入配置
spring.redis.host=localhost spring.redis.port=6379 #指定连接Redis的BD0数据库 spring.redis.database=0 #最大活动连接数 spring.redis.lettuce.pool.max-active=20 #最大空闲连接数 spring.redis.lettuce.pool.max-idle=20 #最小空闲连接数 spring.redis.lettuce.pool.min-idle=2
- Controller
@Controller @RequestMapping("/Redis") public class TestRedisTemplate { @Autowired private RedisTemplate redisTemplate; @RequestMapping("/test") public void testString() { // 调用set()方法创建缓存 redisTemplate.opsForValue().set("name", "燕双嘤"); System.out.println("name: "+ redisTemplate.opsForValue().get("name")); } }
【问题】存储数据可能导致乱码,是因为和redis内部的编码协议出现了问题。
@Configuration public class RedisTemplateConfig { @Autowired private RedisTemplate redisTemplate; @Bean public RedisTemplate testString() { //设置序列化Key的实例化对象 redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置序列化Value的实例化对象 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } }
【创建与读取数据】
- 引入依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
- User类
@Data @AllArgsConstructor @NoArgsConstructor @ToString public class User implements Serializable { private String name; @JsonIgnore private String password; private Integer age; @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8") private Date birthday; @JsonInclude(JsonInclude.Include.NON_NULL) private String desc; }
- Controller
@Controller @RequestMapping("/Redis") public class TestRedisTemplate { @Autowired private RedisTemplate redisTemplate; @RequestMapping("/testUser") public void testUser() { User user = new User(); user.setName("燕双嘤"); user.setPassword("123456"); user.setAge(30); ValueOperations<String, User> operations = redisTemplate.opsForValue(); // 调用set()方法创建缓存 operations.set("燕双嘤", user); // 调用get()方法获取数据 User u = operations.get("燕双嘤"); System.out.println(u.toString()); } } ================================================================== User(name=燕双嘤, password=null, age=30, birthday=null, desc=null)
【删除缓存数据】
@Controller @RequestMapping("/Redis") public class TestRedisTemplate { @Autowired private RedisTemplate redisTemplate; @RequestMapping("/deleteUser") public void deleteUser() { ValueOperations<String, User> operations = redisTemplate.opsForValue(); // 删除缓存 redisTemplate.delete("燕双嘤"); // 判断key是否存在 boolean exists = redisTemplate.hasKey("燕双嘤"); if (exists) { System.out.println("exists is true"); } else { System.out.println("exists is false"); } } }
【缓存超时失效】Redis可以对存入数据设置缓存超时时间,超过缓存时间Redis就会自动删除该数据。这种特性非常适合有时效限制的数据缓存及删除的场景。
@Controller @RequestMapping("/Redis") public class TestRedisTemplate { @Autowired private RedisTemplate redisTemplate; @RequestMapping("/expireUser") public void expireUser() { User user = new User(); user.setName("燕双嘤"); user.setAge(30); ValueOperations<String, User> operations = redisTemplate.opsForValue(); // 创建缓存并设置缓存失效时间 operations.set("燕双嘤", user, 10000, TimeUnit.MILLISECONDS); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } // 10秒后判断缓存是否存在 boolean exists = redisTemplate.hasKey("燕双嘤"); if (exists) { System.out.println("exists is true"); } else { System.out.println("exists is false"); } try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } // 10秒后判断缓存是否存在 exists = redisTemplate.hasKey("燕双嘤"); if (exists) { System.out.println("exists is true"); } else { System.out.println("exists is false"); } } }
5.3,使用Spring Data Redis
由于Spring Data是高层次的抽象,而Spring Data Redis只是属于底层的具体实现,因此Spring Data Redis也提供了与前面Spring Data完全一致的操作。
- DAO接口需要继承CrudRepository,Spring Data Redis能为DAO组件提供实现类。
- Spring Data Redis支持方法名关键字查询,只不过Redis查询的属性必须被索引过。
- Spring Data Redis同样支持DAO组件添加自定义的查询方法——通过添加额外的接口,并为额外的接口提供实现类,Spring Data Redis就能将该实现类中的方法“移植”到DAO组件中。
- Spring Data Redis同样支持Example查询。
PS:Spring Data Redis支持的方法名关键字查询功能不如JPA强大,这是由Redis底层决定的—Redis不支持任何查询,它只是一个简单的key-value数据库,它获取数据的唯一方式就是根据key获取value。因此它不支持GreaterThan、LessThan、Like等复杂关键字,他它只支持:
- And:比如在接口中可以定义“findByNameAndAge”。
- Or:比如“findByNameOrAge”。
- Is、Equals:比如“findByNameIs”“findByName”“findByEquals”这类表示相同或相等的关键字不加也行。
- Top、First:比如“findFirst5Name”“findTop5ByName”,实时查询前5条记录。
为了处理数据类与Redis之间的映射,Spring Data Redis提供了如下两个注解:
- @RedisHash:该注解指定将数据类映射到Redis的Hash对象。
- @TimeToLive:该注解修饰一个数值类型的属性,用于指定该对象的超时时长。
此外,Spring Data Redis还提供了两个索引注解:
- @Indexed:指定对普通类型的属性建立索引,索引化后的属性可用于查询。
- @GeoIndexed:指定对Geo数据(地理数据)类型建立索引。
spring.redis.host=localhost spring.redis.port=6380 #指定连接Redis的BD0数据库 spring.redis.database=0 #最大活动连接数 spring.redis.lettuce.pool.max-active=20 #最大空闲连接数 spring.redis.lettuce.pool.max-idle=20 #最小空闲连接数 spring.redis.lettuce.pool.min-idle=2
@RedisHash("book") public class Book { @Id private Integer id; //二级索引 @Indexed private String name; @Indexed private String description; private Double price; //定义它的超时时长 @TimeToLive(unit = TimeUnit.HOURS) Long timeout; //省略set,get方法,构造方法 }
Book类使用了@RedisHash("book")修饰,这意味着该类的实例映射到Redis中的key都会增加book前缀。
package main.Dao; import main.Pojo.Book; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.QueryByExampleExecutor; import java.util.List; public interface BookDao extends CrudRepository<Book,Integer>, QueryByExampleExecutor<Book>{ List<Book> findByName(String name); List<Book> findByDescription(String subDesc); }
Dao接口继承了CrudReposity,这是Spring Data对DAO组件的通用要求。此外,该DAO接口还继承了QueryByExampleExecutor,支持Example查询。
package main.Controller; import main.Dao.BookDao; import main.Pojo.Book; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController public class BookController { @Autowired private BookDao bookDao; @PostMapping("/save") public void SaveWithId(Book book){ book.setName("java"); book.setDescription("hello java"); book.setTimeout(5L); book.setId(1); bookDao.save(book); } @PostMapping("/update") public void Update(){ bookDao.findById(1).ifPresent(book -> { book.setName("python"); bookDao.save(book); }); } @PostMapping("/delete") public void Delete(){ bookDao.deleteById(1); } @PostMapping("/findByName") public void FindByName(String name){ bookDao.findByName(name); } @PostMapping("/findByDescription") public void FindByDescription(String description){ bookDao.findByDescription(description).forEach(System.out::println); } }
此外,Spring Data Redis同样支持DAO组件继承额外的接口增加自定义的查询方法。
import java.util.List; import java.util.Map; import main.Pojo.Book; public interface BookCustomDao{ void hmset(String key, Map<String, String> hash); List<Book> customQuery(double startPrice); }
import main.Pojo.Book; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.StringRedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; public class BookCustomDaoImpl implements BookCustomDao{ @Autowired private StringRedisTemplate redisTemplate; @Override public void hmset(String key, Map<String, String> hash){ // 调用opsForHash()获取操作Hash对象的HashOperations,再调用putAll()方法 redisTemplate.opsForHash().putAll(key, hash); } @Override public List<Book> customQuery(double startPrice){ // 调用execute(RedisCallback)执行自定义操作 return redisTemplate.execute((RedisCallback<List<Book>>) connection -> { List<Book> result = new ArrayList<>(); StringRedisConnection conn = (StringRedisConnection) connection; // 查询key为book对应的Set,该Set保存了所有Book对象的id Set<String> ids = conn.sMembers("book"); // 遍历所有Book对象的id for (String idStr : ids){ // 实际Book对象的key遵守格式:"book:id" String objKey = "book:" + idStr; // 读取实际对象映射的Hash对象 Map<String, String> data = conn.hGetAll(objKey); String priceStr = data.get("price"); if (priceStr != null){ var price = Double.parseDouble(priceStr); if (price > startPrice) { Integer id = Integer.parseInt(idStr); // 读取数据,并转换为了Book String name = data.get("name"); String description = data.get("description"); // 将读取的数据封装成Book对象 var b = new Book(name, description, price); b.setId(id); result.add(b); } } } return result; }); } }
5.4,封装Redis(RedisCommands方式)
import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.SetArgs; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import org.springframework.stereotype.Repository; import java.time.Duration; import java.time.temporal.ChronoUnit; @Repository public class RedisTool { private RedisClient redisClient; private StatefulRedisConnection<String, String> conn; public RedisTool() { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1") .withDatabase(0) .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); //创建RedisClient redisClient = RedisClient.create(redisUri); //获取StatefulRedisConnection conn = redisClient.connect(); } public void setContent(String key, String content, int time) { StatefulRedisConnection<String, String> conn = redisClient.connect(); //创建同步模式的RedisCommands RedisCommands<String, String> redisCommands = conn.sync(); //设置超时时长 SetArgs setArgs = SetArgs.Builder.nx().ex(time); //执行Set命令 redisCommands.set(key, content, setArgs); } public String getContent(String key) { StatefulRedisConnection<String, String> conn = redisClient.connect(); //创建同步模式的RedisCommands RedisCommands<String, String> redisCommands = conn.sync(); //执行Get命令 String result = redisCommands.get(key); return result; } public void closeResource() { //关闭StatefulRedisConnection conn.close(); //关闭RedisClient redisClient.shutdown(); } }
5.5,连接多个Redis服务器
SpringBoot:整合持久化层_燕双嘤的博客-CSDN博客JdbcTemplate是Spring提供的一套JDBC模板框架,利用AOP技术来解决使用JDBC时大量重复代码的问题。JdbcTemplate虽然没有MyBatis那么灵活,但是比直接使用JDBC要方便很多。Spring Boot中对JdbcTemplate的使用提供了自动化配置类JdbcTemplateAutoConfiguration。...https://shao12138.blog.csdn.net/article/details/109637942#t18放弃SpringBoot为Redis提供的自动配置后(SpringBoot的自动配置只能帮助连接一个Redis服务器),执行如下步骤:
- 手动配置多组RedisConnectionFactory和RedisTemplate,要连接几个Redis服务器就配置几组。每个RedisConnectionFactory对应连接一个Redis服务器。
- 针对不同的Redis服务器,分别开发相应的DAO组件类,建议放到不同的包下,以便区分。
- 使用@EnableRedisRepositories注解手动开启DAO组件扫描。
其中,@EnableRedisRepositories注解需要指定如下两个属性:
- basePackages:指定扫描哪个包下的DAO组件。
- redisTemplateRef:指定使用哪个RedisTemplate来实现DAO组件的方法。
如果需要对Redis进行更多的定制,则可通过在容器中配置任意个LettuceClientConfigurationBuilderCustomizer实现类的Bean,它可以对Lettuce进行定制化的设置;如果底层使用Jedis,则可配置JedisClientConfigurationBuilderCustomizer实现类定制。