Redis探索之旅(基础)

目录

今日良言:满怀憧憬,阔步向前

一、基础命令

1.1 通用命令

1.2 五大基本类型的命令

1.2.1 String

1.2.2 Hash

1.2.3 List

1.2.4 Set

1.2.5 Zset

二、过期策略以及单线程模型

2.1 过期策略

2.2 单线程模型

2.3 Redis 效率为什么这么高

三、Java 客户端操作 Redis

四、Spring 操作 Redis


🎅🏻 今日良言:满怀憧憬,阔步向前

🎅🏻 一、基础命令

1.1 通用命令

“一个优秀的命令”,不仅会使得企业相关软件卡死,而且会让程序猿“成功” 带走自己的年终奖,若出于某些目的性的行为,可能会喜提银手镯一副,吃上国家饭,比如“故意删库跑路”。那么,作为一个兢兢业业的程序猿,在操作 redis 的时候,该如何避免自己的年终奖被一波带走呢?接下来,可要仔细学习下面这些命令。

keys

首先,keys 隆重登场。

语法:

keys pattern

keys 返回所有满足样式(pattern)的key,同时,它也支持如下统配样式。

h?llo 

?可以替换成任意字符,比如匹配:hallo、hbllo....

h*llo

* 可以替换0个或多个相同字符,比如匹配:hllo、heeello

h[ae]llo 

匹配包含[ ] 括号中的字符的key,比如匹配:hallo、hello

h[^e]llo

匹配除了e 字符的key,比如匹配:hallo、hbllo.. 但是不包含hello

h[a-f]llo

匹配包含 a-f 区间的字符,比如:hallo、hbllo、hfllo 

示例:

命令有效版本:1.0.0之后

时间复杂度:O(N)

返回值:匹配 pattern 的所有key

 在生产环境上,一般都会禁止使用keys命令,尤其是 keys * 这个大杀器,因为 keys * 这个命令会查询 redis 所有的 key,生产环境上的 key 可能会非常的多,而 redis 是一个单线程的服务器,执行 keys 的时间非常长,会导致 redis 服务器被阻塞了,无法给其它的客户端提供服务,这样的后果是灾难性的,redis 经常会被用于缓存,是替数据库抵挡大量的请求,万一 keys * 将 redis 阻塞住了,此时其它的查询 redis 操作就超时了,此时这些请求会直接查询数据库,大量请求同时访问数据库,可能会导致数据库宕机,如果程序猿没有及时发现且没有及时恢复的话,年终奖就被一波带走了,更严重的话,工作也就没有了。

exists

判断某个key是否存在

语法:

exists key [key....]            【一次性可以判断多个key】

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:key 存在的个数

 del

删除指定的key

语法:

del  key [key....]          【一次性可以删除多个key】

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:删除的 key 的个数

expire 

为指定的key添加秒级的过期时间

 语法:

expire key seconds

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:1表示设置成功,0表示设置失败

 ttl (time to live)  

获取指定key的过期时间,秒级

思考题:这里的 ttl 和 IP 协议报头中的字段 TTL 有什么区别?

这里的ttl 获取的是时间,单位:秒。而 IP 协议中的 TTL 不是用时间衡量的,而是用次数。 

语法:

ttl key

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:剩余过期时间。 -1 表示没有关联过期时间,-2 表示 key 不存在

 注:expire 和 ttl 命令都有对应的支持毫秒为单位的版本:pexpire 和 pttl

type

返回 key 对应的数据类型

语法:

type key

示例:

命令有效版本: 1.0.0之后

时间复杂度:O(1)

返回值: none、string、list、set、zset、hash、stream 

以上就是 redis 中几个基本的全局命令,熟练掌握有助于把握年终奖哦。


1.2 五大基本类型的命令

本小节主要介绍 redis 中的五大基本类型的命令及其内部编码和使用场景

1.2.1 String

首先介绍一下 String类型。

字符串类型是 Redis 最基础的数据类型,注:此String 非彼 String(java)。

Redis 所有的 key 都是字符串,只是 value 的类型有所差异。

Redis 中的字符串,直接是按照二进制的方式进行存储的,不会做任何的编码转换,存啥取啥。Redis 不仅可以存二进制数据(图片、音频、视频...),还可以存:整数、文本数据、JSon、xml、普通的文本字符串...

由于二进制数据音视频的体积比较大,于是 Redis 对于 String 类型的 value 进行了大小限制,最大为512MB,主要是为了控制单个值对内存的占用,确保 Redis 的性能。因为 Redis 是单线程模型,希望进行的操作都是比较快速,如果操作过大的 value,可能会影响整个 Redis 的响应时间。

从三个方面来介绍:

1)命令

set

 将String类型的 value 设置到 key 中,如果 key 已经存在,则覆盖 key 的value值(包括不同数据类型)

语法:

set key value [expiration EX seconds|PX milliseconds] [NX|XX]

上述语法格式说明:

[ ]相当于一个独立的单元,表示可选项(可有可无),其中 | 表示 或者 的意思,多个只能选择一个,[ ] 和 [ ] 之间可以同时存在 

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值: 设置成功返回OK。set 如果指定了 nx 或者 xx 但条件不满足,set 不会执行,返回(nil)

set 命令支持多种选项来影响它的行为:

EX seconds : 使用秒作为单位设置 key 的过期时间。

PX milliseconds : 使用毫秒作为单位设置 key 的过期时间

NX:当 key 不存在的时候才设置。

XX:当 key存在的时候才设置,覆盖 value 值

示例:

这里是设置了key2 的 value 值为v2,并且通过ex设置过期时间为10s,并且只有当 key2 不存在的时候才设置。

10s 后 key2 过期,此时查询结果返回 (nil):

注: 由于上述带选项的 set 命令可以被 setnx、setex、psetex 等命令代替,所以之后的版本中,Redis 可能进行合并。 

get

获取对应的 key 的 value 值,如果 key 不存在,返回 (nil),如果 value 的数据类型不是 String,会报错

语法:

get key

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:key 对应 value ,nil 或者 报错

mget

一次性获取多个 key 的值,如果对应的 key 不存在或者对应的数据类型不是 String,返回 (nil)

语法:

mget key[key1 key2 ....]

示例:

命令有效版本: 1.0.0 之后

时间复杂度: O(N)N是 key 的个数

返回值:对应 value 的列表

mset

一次性设置多个 key 的值

语法:

mset key v1 key2 v2 ....

示例:

命令有效版本:1.0.1

时间复杂度: O(N) N 是 key 的个数

返回值:永远是 OK

mset 和 mget 是操作一组键值对(批量操作)

 mget 和 get 的区别:

如图所示,使用mget/mset 可以很好的减少网络时间,提高相较于 n 次 get/set 的性能要高,使用批量操作,可以有效地提高业务处理效率,但是需要注意,批量操作也是有弊端的,每次批量发送的键的数量并不是毫无节制的,这会使得单一命令执行时间过长,进而导致 redis 阻塞。

setnx 

在 key 不存在的情况下设置 key-value

语法:

setnx key

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:设置成功返回 1,设置失败返回 0

setex

给 key 设置 value,同时设置过期时间,其效果相当于: set key value 后 expire seconds

语法:

setex key seconds value

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:永远都是OK

psetex 是设置毫秒级别的过期时间,语法setex相同

setnx、setex、psetex 是针对 set 的一些常用命令进行了缩写,这样的好处就是让操作更符合人的直觉,让使用者的学习成本更低。

接下来介绍一些加减 value 的操作

incr

 将 key 对应的 string 表示的数字加1。如果 key 不存在,则视为 key 对应的 value 是 0,若 key 对应的 string 不是一个整数或者范围超过了 64位有符号整数,则报错。

语法:

incr key

示例:

命令有效时间:1.0.0之后

时间复杂度:O(1)

返回值:integer 类型加完后的值

incrby

将 key 对应的 string 表示的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0,若 key 对应的 string 不是一个整数或者范围超过了 64位有符号整数,则报错。

语法:

incrby key decrement

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:integer 类型加完后的值

decr

 将 key 对应的 string 表示的数字减1。如果 key 不存在,则视为 key 对应的 value 是 0,若 key 对应的 string 不是一个整数或者范围超过了 64位有符号整数,则报错。

语法:

decr key

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:integer 减完之后的值

decrby

将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0,若 key 对应的 string 不是一个整数或者范围超过了 64位有符号整数,则报错。

语法:

decrby key decrement

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:integer 类型减完之后的值

incrbyfloat

将 key 对应的 string 表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是⼀个浮点数,则报 错。允许采用科学计数法表⽰浮点数。

语法:

incrbyfloat key increment

示例:

命令有效版本:2.6.0之后

时间复杂度:O(1)

返回值:加/减完之后的值

append

如果 key 存在并且是 String 类型,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 set 命令。

语法:

append key  value

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1).追加的字符串一般较短,可以视为O(1)

返回值:追加完后 string 的长度

getrange

返回 key 对应的 value 的子串,由 start 和 end 确定(左闭右闭)。可以使用负数表示,-1 代表倒数第一个字符,-2 代表倒数第二个字符,其他的与之类似。超过范围的偏移量会根据 value 的长度调整成正确的值。

语法:

getrange key start end

示例:

命令有效版本:2.4.0之后

时间复杂度:O(N).N是[start,end]区间的长度,由于这个区间较短,一般视为O(1)

返回值:string 类型的字符串

setrange

覆盖字符串的一部分,从指定的偏移量开始

语法:

setrange key offset value

示例:

命令有效版本:2.0.0之后

时间复杂度:O(N). N为 value 的长度,由于一般给定的 value 较短,通常视为O(1)。

返回值:替换后的 string 长度

strlen

获取指定的 key 的长度,如果 key 不是 String 类型,会报错,如果 key 不存在,返回0

语法:

strlen key

示例:

命令有效版本:2.2.0 之后

时间复杂度:O(1)

返回值:对应的 string 的长度,key 不存在返回0


2)内部编码

字符串类型的编码有三种:

  • int:8个字节的长整数
  • embstr:小于等于39个字节的字符串
  • raw:大于39个字节的字符串

redis 会根据当前 value 的类型和长度动态决定使用哪种内部编码实现

注:从 redis 4.0开始,当 value 的字节数小于等于44是embstr,大于44字节是raw

通过如下命令可以查看当前内部编码:

object encoding key

示例:

3)使用场景

缓存(Cache)功能

redis 可以当做缓冲层,mysql 作为存储层,此时,绝大部分请求的数据都是从 redis 中获取。由于 redis 支持高并发,所以缓存通常可以起到加速读写和降低后端压力的作用。

计数功能

许多应用都会使用 redis 作为计数的基础工具,它可以实现快速计数,查询缓存的功能,同时,数据可以异步处理或者落地到其他数据源。例如:视频网站的播放次数可以使用 redis 来完成,用户每播放一次数据,相应的视频播放数就会自增1。

共享会话

⼀个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到
不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同⼀台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题用户无法容忍的,为了解决这个问题,可以使用 redis 将用户的 Session 信息进行集中管理,在这种模式下,只要保证 redis 是高可用和可扩展的,无论用户被均衡到哪台 Web 服务器上,都可以集中从 redis 中查询、更新 Session 信息。

手机验证码

很多应用出于安全考虑,会在每次登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码进行验证,从而确认是否是用户本人,为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过五次。

以上是 String 类型的相关介绍。


1.2.2 Hash

接下来介绍 Hash (哈希)类型。

在 redis 中,Hash 类型是指值本身又是一个键值对结构,如下图:

哈希类型中的映射关系通常称为 field-value,用于区分 redis 的整体键值对(key - value)

1)命令

hset

设置 hash 中指定字段(field)的值(value)

语法:

hset key field value [field value....]   可设置多组

 示例:

命令有效版本:2.0.0之后

时间复杂度:插入一组 field 为O(1),插入多组为 O(N)

返回值: 添加的字段(field)的个数

hget

获取指定字段(field)的值(value)

语法:

hget key field

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1)

返回值: field 对应的值。如果键 key 或者 field 不存在,返回(nill)

 hexists

判断 hash 中是否有指定的字段(field)

 语法:

hexists key field

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1)

返回值: 存在返回1,否则0

hdel

删除 hash 中指定的字段(field)

语法:

hdel key field [field...]   可以删除多组

示例:

命令有效版本:2.0.0之后

时间复杂度:删除一个为O(1),删除 N 个为O(N)

返回值: 本次删除的 field 的个数

hkeys

获取 hash 中所有字段(field)

语法:

hkeys key

示例:

命令有效版本:2.0.0之后

时间复杂度:O(N)N是 field 的个数

返回值: 字段列表

hvals

获取 hash 中所有的值

语法:

hvals key

示例:

命令有效版本:2.0.0之后

时间复杂度:O(N)N是 field 的个数

返回值: 值列表

hgetall

获取所有字段(field)以及其对应的值

语法:

hgetall key

示例:

命令有效版本:2.0.0之后

时间复杂度:O(N)N是 field 的个数

返回值: 字段和对应的值

hmget

一次性获取 hash 中多个字段的值,查询结果中的 value 的顺序和 field 的顺序相匹配

语法:

hmget key field  [field]

实例:

命令有效版本:2.0.0之后

时间复杂度:只查询⼀个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.

返回值: 字段对应的值或者nill

 看到这里,各位程序员们是不是有疑问:有没有 hmset,可以一次性设置多个 field 和 value 呢?(什么?没有?那不行!没有也得有,不然小马怎么凑字数,弱弱的调侃一句)

答案是有的,但是并不需要。因为 hset 可以一次性设置多个 field 和 value。如下图:

 

 注:

1)在上述的hkeys,hvals,hgetall 都是存在一定风险的,当 hash 中元素过多时,执行的时间比较长,从而阻塞 redis。

2)如果只想获取部分 field ,可以使用 hmget ,如果一定要获取所有的 field,可以尝试命令 hscan,该命令采用渐进式遍历哈希类型,也就是敲一次命令,遍历一小部分,再敲一次命令,再遍历一小部分,连续执行多次,就可以完成整个的遍历过程了,化整为零。

hlen

获取 hash 中所有字段的个数

语法:

hlen key

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1)

返回值: 字段个数

hsetnx

在字段不存在的情况下,设置 hash 中的字段和值

语法:

hsetnx key field value

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1)

返回值: 1设置成功,0设置失败

hincrby

将 hash 中字段对应的值添加指定的值

语法:

hincrby key field increment

示例:

命令有效版本:2.0.0之后

时间复杂度:O(1)

返回值: 该字段对应的值变化后的值,如果对应的值不是整数类型,会报错

hincrbyfloat

将 hash 中字段对应的值添加指定的浮点数

语法:

hincrbyfloat key field increment

示例:

命令有效版本:2.6.0之后

时间复杂度:O(1)

返回值: 该字段对应的值变化后的值,如果对应的值不是整数类型,会报错

以上就是 Hash 类型的命令。


2)内部编码

Hash 类型的内部编码有两种:

  • ziplist(压缩列表)   
  • hashtable(哈希表)

当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认是512个)且同时所有值小于 hash-max-ziplist-value 配置(默认是64字节)时,redis 会使用 ziplist 作为哈希的内部编码,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。

当哈希类型不满足 ziplist 的条件时,redis 会使用 hashtable 作为哈希的内部编码,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度都是O(1)。

上述配置在 redis.conf 文件中都可以查看,并进行修改:

 示例:

3)使用场景

作为缓存

String 也是可以作为缓存使用的,但存储结构化的数据(类似于数据库 表 这样的结构),使用 hash 类型更合适一些。如下:

 以上就是 Hash 类型的相关介绍。


1.2.3 List

接下来介绍 List 类型,List 用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储2^32 - 1 个元素。在 redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角,在实际开发中有很多应用场景。如下图:

1)命令

lpush

将一个或者多个元素插入到队列的左侧(头插)

语法:

lpush key element  [element....]  

示例:

命令有效版本:1.0.0之后

时间复杂度:插入一个元素O(1),插入多个元素O(N),N为元素的个数

返回值:插入后 list 的长度

lpushx

key 存在时,插入一个或者多个元素到队列的左侧(头插),key 不存在,返回0

语法:

lpushx key element[element...]

示例:

命令有效版本:2.0.0之后

时间复杂度:插入一个元素O(1),插入多个元素O(N),N为元素的个数

返回值:插入后 list 的长度

 rpush

将一个或者多个元素插入到队列的右侧(尾插)

语法:

rpush key element[element..]

示例:

命令有效版本:1.0.0之后

时间复杂度:插入一个元素O(1),插入多个元素O(N),N为元素的个数

返回值:插入后 list 的长度

rpushx

key 存在时,插入一个或者多个元素到队列的右侧(尾插),key 不存在,返回0

语法:

rpushx key element[element...]

示例:

命令有效版本:2.0.0之后

时间复杂度:插入一个元素O(1),插入多个元素O(N),N为元素的个数

返回值:插入后 list 的长度

 lrange

获取 start 和 end 区间内的元素,左闭右闭

语法:

lrange key start stop

示例:

 命令有效版本:1.0.0之后

时间复杂度:O(N)

返回值:指定区间的元素

 lpop

从 list 左侧取出元素(头删)

语法:

lpop key 

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:取出的元素或者(nill)

 rpop

从 list 右侧取出元素(尾删)

语法:

lpop key

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:取出的元素或者(nill)

lindex

获取从左边数第 index 位置的元素

语法:

lindex key index

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:取出的元素或者(nill)

linsert

在指定元素前/后插入元素

语法:

linsert key <before| after> pivot element

示例:

注:如果这里指定的值有多个,linsert 插入的时候,按照从左到右,找到第一个符合指定值的位置即可。

命令有效版本:2.2.0之后

时间复杂度:O(N)

返回值:插入后 list 的长度,插入失败返回 -1

llen

获取 list 的长度

语法:

llen key

示例:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:list 的长度

lrem

 从左到右删除指定元素,可以指定删除的个数

语法:

lrem key count element

示例:

对于这里的 count 要分情况讨论:

count > 0 : 从头到尾进行删除指定元素

                  删除了前两个 1

count < 0 : 从尾到头进行删除执行元素

                   删除了最后一个 2

count = 0 : 删除所有指定元素

                  删除了所有的 3

命令有效版本:1.0.0之后

时间复杂度:O(N) N 是删除元素的个数

返回值:删除元素的个数

ltrim 

保留 start 和 stop 区间内的所有元素(区间外的元素全部删除)

语法:

ltrim key start stop

示例:

 以上是 list 类型的一些基本命令,接下来介绍一下阻塞版本的命令。

blpop 和 brpoplpop 和 rpop阻塞版本,和对应的非阻塞版本的作用基本一致,除了:

 当列表中无元素时,阻塞版本会根据 timeout 阻塞一段时间,而非阻塞版本会立即返回 nill,在阻塞期间,redis 可以执行其它命令,但要求执行该命令的客户端会表现为阻塞状态。

blpop

lpop 的阻塞版本

语法:

blpop key[key..] timeout

示例:

当 list 有元素时:

当 list 无元素时,会阻塞:

如果当前客户端执行 blpop 命令时阻塞了,在阻塞期间,其它客户端往这个 list 中插入了数据,当前客户端此时就会获取元素,不再阻塞:

客户端1:

客户端2:

当客户端 2 的命令执行后,客户端 1将返回结果,不再阻塞:

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:取出的元素或者nill

brpop

rpop 的阻塞版本

语法:

brpop key[key..] timeout

示例:

与blpop 操作一样,这里不进行实操掩饰,铁子们自己动手实操,老话说的好:眼过千遍不如受过一遍(好吧,其实是我懒得敲了 0.0)

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:取出的元素或者nill

以上就是 List 类型的相关命令介绍


2)内部编码

在 reids 3.2版本之前,List 类型的内部编码有两种:

  • ziplist (压缩列表)
  • linkedlist(链表)

ziplist 把数据按照更紧凑的压缩形式进行表示,可以做到节省空间,但是当元素个数多了,操作起来效率会降低。当列表的元素个数小于 list-max-ziplist-entries 配置(默认是512个),同时列表中每个元素的长度都小于 list-max-ziplist-entries 配置(默认是64字节)时,redis 会选用ziplist 来作为内部编码。

当列表类型无法满足 ziplist 的条件时,redis 会使用 linkedlist 作为列表的内部实现。

 在 redis 3.2版本开始,内部编码采用 quicklist

quicklist 相当于是链表和压缩列表的组合,整体还是一个链表(双向链表),链表的每个节点是一个压缩列表。quicklist的行为可以通过配置参数list-max-ziplist-size来调整,该参数决定了压缩列表的最大大小。当元素数量超过这个配置值时,Redis会创建一个新的压缩列表节点。

3)使用场景

消息队列

每次只有一个消费者能抢到该元素,对于三个消费者来说,谁先执行 brpop 命令,谁就先拿到这个元素。 

以上就是对 List 类型的相关介绍。


1.2.4 Set

接下来介绍 Set(集合) 类型

集合类型也是保存多个字符串类型的元素的,但和列表元素有所不同,主要有以下区别:

1.元素之间是无序的

2.元素不允许重复

一个集合最多可以存储 2^32 -1 个元素。 Redis 除了支持集合内的增删改查操作,还支持集合间的并集、交集、差集。集合类型的结构如下图:

老样子,先来介绍命令

1)命令

sadd

将一个元素或者多个元素添加到 set 中,无法添加重复元素

语法:

sadd key member [member...]

示例:

只添加成功了三个元素

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:本次添加成功的元素的个数

 将 set 中的元素称为 member

 smembers

获取 set 中的所有元素,元素之间是无序的。

语法:

smembers key

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:集合中的所有元素

 sismember

判断某个元素在不在 set 中

语法:

sismember key member

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:在返回1,不在或者 key 不存在返回0

scard

获取 set 中的元素个数

语法:

scard key

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:set 中元素的个数

spop

随机删除并返回一个或者多个元素

语法:

spop key [count]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N) N是count

返回值:取出的元素

smove

移动元素:将一个元素从源 set 中取出并放入到目标set 中

语法:

smove source destination member        

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(1) 

返回值:1 表示成功,0 表示失败

 srem

将指定的元素从 set 中删除

语法:

srem key member[smember...]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)  N 是待删除元素的个数

返回值:本次删除元素的个数

以上是集合内部的操作,接下来介绍集合间的操作:交集 并集 差集。

在小学(或许初中)时,数学老师就教过我们这个概念,这里简单举一个例子:

集合一:1 2 3  4       集合二: 1 4 5 6

交集(两个集合都有的元素):1  4

并集(两个集合所有的元素,去除重复元素): 1 2 3 4 5 6

差集:

        集合一对集合二求差集:2 3

        集合二对集合一求差集:5 6

接下来介绍相关命令:

并集操作

sunion

获取给定 set 的并集中的元素

语法:

sunion key [key...]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:并集的元素

sunionstore

获取给定的 set 的并集并保存到目标 set 中

语法:

sunionstore destination key [key...]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:并集的元素个数

交集操作

sinter

获取给定 set 的交集元素

语法:

sinter key[key..]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:交集的元素

sinterstore

 获取给定的 set 的交集并保存到目标 set 中

语法

sinterstore destination key[key...]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:交集的元素个数

 差集操作

sdiff

获取给定 set 的差集中的元素

语法:

sdiff key[..]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:差集的元素

sdiffstore

 获取给定的 set 的差集并保存到目标 set 中

语法:

sdiffstore destination key[key..]

示例:

命令有效版本:1.0.0 之后

时间复杂度:O(N)N是给定的所有集合总的元素个数 

返回值:差集的元素个数

以上就是 Set 类型的相关命令。


2)内部编码

Set 类型的内部编码有两种:

  • intset(整数集合)
  • hashtable(哈希表)

  当集合中的元素都是整数并且元素的个数小于set-max-intset-entries 配置(默认512个)时,redis 会选用 intset 作为集合的内部实现,从而减少内存的使用。

  当集合类型不满足 intset 的条件时,redis 会使用 hashtable 作为集合的内部实现。

上述配置在redis.conf 文件中依旧可以找到:

3)使用场景

 保存用户标签

Set 比较典型的使用场景就是标签,例如用户1对美女感兴趣,用户2对体育类感兴趣,这些特征都可以抽象成标签,可以分析清楚用户特征以后,再投其所好,推送用户喜欢的内容。同时,有了这些标签数据以后,就可以得到喜欢同一个标签的用户(通过 set 的交集操作),这些数据对于增强用户体验和用户黏度非常有帮助。

计算用户之间的共同好友

基于“集合求交集”可以计算出用户之间的共同好友,基于这个还可以做一些好友推荐,比如:A和B是好友,A和C是好友,B和C和D都是好友,系统就会把D推荐给A。

以上就是对于 Set 类型的相关介绍。


1.2.5 Zset

最后介绍 Zset(有序集合)。

这里的有序指的是:升序/降序,通过给 zset 中的 member 同时引入一个属性(分数 score 浮点类型)来作为排序规则,每个 member 都会安排一个分数,进行排序的时候,就根据 score 的大小来进行升序/降序 排序。

对于 set 集合来说,其 member 是无序的,且不能重复,而 zset 保留了 member 不能重复的特点,同时根据 score 来维护有序性。

以三国中的武将的武力值来理解 Zset 的结构:

1)命令

zadd

添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负极限也是合法的。

语法:

zadd key[NX | XX ] [GT | LT] [CH] [INCR] score member [score member...]

member 和 score 称为一个 “pair”,这个“pair” 与键值对(key-value)不同,键值对中,是有明确的“角色区分”,谁是键,谁是值是明确的,一定是根据“键”找“值”,而对于有序集合来说,既可以通过 member 找到对应的 score,又可以通过 score 找到匹配的 member。

解释一下这里的一些选项:

[NX | XX]

XX:当member存在时,才进行修改,不会添加新的member。

NX:当member不存在时,才进行添加 member操作,不会修改已经存在的 member。

不加上述这两个选项的时候,如果当前 member 不存在,此时就会创建新的 member ,如果当前 member 已经存在,此时就会更新分数。

[GT | LT]

LT:less than,更新分数时,如果给定的新分数小于当前的分数,此时更新成功,否则不更新。

GT:greater than,更新分数时,如果给定的新分数大于当前的分数,此时更新成功,否则不更新。

这个选项在 redis 6.2 版本引入,由于小马的 redis 版本更早,因此这个选项不做演示,铁铁们自己尝试一波。

[CH]

本来,zadd 命令的返回值是新添加到有序集合的 member 数量,而当指定了 CH 选项时,返回值将更改为发生变化的成员总数,这包括新添加的成员以及修改了分数的已有成员。如果命令中执行的成员的分数和现有的成员拥有相同的分数,则这些成员不计入变化的成员内。

总的来说,CH 选项提供了对 zadd 命令返回值的额外控制,使得用户能够获得有关数据变化的更精确信息,特别是在需要跟踪或记录数据变动情况时非常有用。

[INCR]

对指定成员(member)的分数进行增加。

举例:

不加任何选项

使用 NX

使用 XX

使用CH

这里小马想要修改 zhansgan 的分数为 99.6,查询结果竟然是99.5999999...,这是为什么呢?

redis 会扣分!!!!(好吧,其实并不是)

真实原因是因为浮点数在计算机中的表示和存储方式导致的精度问题,某些十进制小数无法精确地用二进制表示,因此会出现一些舍入误差。

使用 [INCR]

命令有效版本:1.2.0之后

时间复杂度:O(log(N)) 

返回值:本次添加成功的元素的个数

zcard

获取 zset 中的元素个数

语法:

zcard key

示例:

命令有效版本:1.2.0之后

时间复杂度:O(1)

返回值:zset 中的元素个数

zcount

返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含在内的(可以不包含)。

语法:

zcount key min max

示例:

以上是边界包含的情况,下面演示不包含边界的情况。

在以前的学习中,对于区间而言,( 表示开区间,不包含,[ 表示闭区间,包含,那么,redis 这里也是这样吗,动手实践操作一下(毕竟时间是检验真理的唯一标准),如下图:

WTF,竟然报错!!!

那么,到底如何排除边界呢?

经过一番学习,没想到这里的排除边界设计竟如此 0 疼,如下图:

一个好的设计,应当是符合直觉的,让人能够望文生义,越符合直觉,则学习成本越低,显然,上述这个设计并不是一个好的设计,但是,由于大牛们已经设计好了,我们只能遵守这样的规则,只能将错就错。

可能会有铁铁有疑惑,为什么经过这么长时间的演变,没有人提出来修改呢?

这里关乎到兼容性,由于 redis 已广泛使用,一旦在新版本中引入和之前版本不兼容的特性,成本是非常高的。

zset 内部会记录每个元素当前的“排行”/“次序”,查询到元素,就直接知道了该元素的“次序”,就可以直接把 max 对应的元素次序和 min 对应的元素次序(下标)相减,这样就可以得到这个区间内的元素个数。

在浮点数中,有两个特殊的数值:inf 表示无穷大,-inf表示负无穷大,zset 中也支持:

命令有效版本:2.0.0之后

时间复杂度:O(log(N))

返回值:满足条件的元素个数

zrange

返回指定区间中的元素,分数按照升序,带上选项 WITHSCORES 会将分数也带上。

语法:

zrange key start stop [withscores]
示例:

命令有效版本:1.2.0之后

时间复杂度:O(log(N)+M)

返回值:区间内的元素列表

 zrevrange

返回指定区间中的元素,分数按照降序,带上WITHSCORES 可以显示分数。

语法:

zrevrange key start stop [WITHSCORES]

示例:

命令有效版本:1.2.0之后

时间复杂度:O(log(N)+M)

返回值:区间内的元素列表

zrangebyscore

返回分数在 min 和 max 之间的分数,默认情况下包含 min 和 max

(该命令可能在6.2.0之后废弃,并且功能合并到 zrange 中)

语法:

zrangebyscore key min max [WITHSCORES]

示例:

排除边界

zpopmax

删除并返回分数最高的 count 个元素

语法:

zpopmax key [count]     不写count 默认返回一个

示例:

如果存在多个元素,分数相同,同时为最大值,此时执行zpopmax命令仍然删除其中一个元素,分数虽然是主要因素,但是当分数相同时,会按照 member 字符串的字典序决定先后。

命令有效版本:5.0.0之后

时间复杂度:O(log(N)* M)

返回值:分数和元素列表

bzpopmax 

zpopmax的阻塞版本,类似与 List 类型中的 blpop brpop。

有序集合也可以视为是一个“优先级队列”,每个 key 都是一个有序集合,阻塞也是在有序集合为空的时候出发,当有其它客户端往有序集合中插入元素时,获取元素停止阻塞。

语法:

bzpopmax key [key..] timeout

timeout 表示最多阻塞时长,单位是s,支持小数形式,0.1表示100ms

示例:

客户端1

此时阻塞

客户端2,往key(有序集合)中插入数据

查看客户端1,停止阻塞,返回分数最大的数据

命令有效版本:5.0.0之后

时间复杂度:O(log(N))

返回值:弹出的元素和分数        

zpopmin

删除有序集合中,分数最小的元素

语法:

zpopmin key[count]

示例:

命令有效版本:5.0.0之后

时间复杂度:O(log(N)* M)

返回值:分数和元素列表

bzpopmin

zpopmin 的阻塞版本

语法:

bzpopmin key[key...] timeout

示例:

客户端1阻塞

客户端2添加元素

查看客户端1:

命令有效版本:5.0.0之后

时间复杂度:O(log(N))

返回值:分数和元素列表

zrank

返回指定元素的排名,升序(zrank得到的下标是从前往后算的)

语法:

zrank key member

示例:

命令有效版本:2.0.0之后

时间复杂度:O(log(N))

返回值:排名

zrevrank

返回指定的元素的排名,降序

语法:

zrevrank key member

示例:

命令有效版本:2.0.0之后

时间复杂度:O(log(N))

返回值:排名

zscore

返回指定 member 的分数

语法:

zscore key member

示例:

命令有效版本:1.2.0之后

时间复杂度:O(1)

返回值:分数,member 不存在返回nill

zrem 

删除指定的元素

语法:

zrem key member[member...]

示例:

命令有效版本:1.2.0之后

时间复杂度:O(M*log(N))

返回值:删除成功的 member 的个数

 zremrangebyrank

删除指定区间中元素,左闭右闭

语法:

zremrange key start stop

示例:

命令有效版本:2.0.0之后

时间复杂度:O(M+log(N)) M 是区间的元素个数

返回值:本次操作删除的元素个数

zremrangebyscore

按照分数删除指定范围的元素,左闭右闭

语法:

zremrangebyscore key min max

示例:

不排除边界

排除边界

命令有效版本:1.2.0之后

时间复杂度:O(M+log(N)) M 是区间的元素个数

返回值:本次操作删除的元素个数

zincrby 

为指定元素关联的分数添加指定值

语法:

zincrby key increment member

示例:
 

zincrby 不仅仅只修改分数,也能移动元素为孩子,保持整个有序集合始终保持升序。

命令有效版本:1.2.0之后

时间复杂度:O(log(N))

返回值:添加后元素的分数

以上是有序集合内部的操作命令,从 redis 6.2开始支持集合间操作,即 交集 并集 差集,对应的命令分别是: zinter  zunion  zdiff 。 由于小马的 redis 是 5版本,因此这几个命令不做介绍,原理和 Set 类型集合间操作相同,版本匹配的烙铁可以尝试一波。

这里介绍另外两个集合间操作:

zinterstore 

求交集操作,结果保存在另一个 key 中

语法:

zinterstore destination numkeys key [key..] [WEIGHTS weight [weight...]] [AGGREGATE <SUM | MIN | MAX>]

解释一下上述一些选项:

numkeys

整数,描述了后面有几个key参与交集运算,这里主要是为了明确后面的选项是从哪里开始,避免选项和 keys 混淆

WEIGHTS

权重,有序集合是带有分数的,此处的权重相当于一个系数,会乘以当前的分数。

[AGGREGATE <SUM | MIN | MAX>]

求交集时,如果 member 相同,进行合并之后的最终分数的计算方法,sum 表示求和,max 表示取最大,min表示取最小。

示例:

key1 和 key2中的元素

执行 zinterstore 命令

由于 key1 的zhangsan的分数是10,乘以1还是10,key2 的zhangsan的分数是15,乘以2是30,最终求和,于是 key3 的zhangsan的分数就是 10 + 30 = 40

再来一个示例:

不指定权重

命令有效版本:2.0.0之后

时间复杂度:O(N*K)+O(M*log(M)) N 是输入的有序集合中, 最小的有序集合的元素个数; K 是输入了几个有序集合; M 是最终结果的有序集合的元素个数

返回值:目标集合中的元素个数

zunionstore

求并集,和 zinterstore 用法基本一致 ,这里不做介绍,铁铁们自己来尝试,眼过千遍不如手过一遍对吧?

 小提示:

以上就是有序集合的相关命令。


2)内部编码

有序集合的内部编码主要有两种:

  • ziplist(压缩列表)
  • skiplist(跳表)

当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value 配置(64字节)时,redis 会用ziplist 来存储,节省空间。

当不满足 ziplist 条件时,也就是元素过多或者单个元素的体积较大时,会使用 skiplist 来进行存储。

 上述配置文件在 redis.conf 依旧可以找到

对跳表不理解的铁子可以阅读以下文章,进行理解:

跳表(Skip List)-CSDN博客

3)使用场景

zset 最关键的应用场景是:排行榜系统。如下:

1.微博热榜

2.游戏天梯排行

3.成绩排行

对于排行榜系统而言,其关键要点就是:用来排行的“分数”,是实时变化的,使用 zset 来完成上述操作,非常简单。比如:游戏天梯排行,只需要把玩家信息和对应分数给放到有序集合中即可,这样就会自动生成一个排行榜,随时可以按照排行(下标)、按照分数进行范围查询,随着分数的改变,也可以方便比较,zincrby 修改分数,排行也会自动调整。

以上就是 redis 的 Zset 类型的相关介绍。


 补充:

渐进式遍历

 在前面已经提到过,keys 一次性把 redis 中的所有 key 都获取到,这个操作比较危险,可能一下得到太多的 key,阻塞 redis 服务器。

通过渐进式遍历就可以做到,既能够获取到所有的 key,同时又不会阻塞 redis 服务器。这里的渐进式遍历,并不是一次获取到所有的 key,而是每一次执行一次命令,只获取其中的一小部分,这样的话就保证这次操作不会太卡,要想得到所有的 key,就需要遍历很多次了,多次执行渐进式遍历命令即可。

渐进式遍历其实是一组命令,这一组命令的使用方法是一样的,其中代表命令是:

scan

语法:

scan cursor [MATCH pattern] [COUNT count]

cursor

是指光标,指向了当前遍历的位置,光标设置成 0 表示这次遍历是从头开始获取,cursor 不能理解成下标,cursor 不是一个连续递增的整数,它仅仅只是一个字符串,而 redis 服务器知道这个光标对应的元素位置。

[MATCH pattern] 

和 keys 的 pattern 一样。

[COUNT count]

限制此次遍历能够获取到多少个元素,默认是10,但是这里的 count 和 mysql 中的 limit 并不一样,limit 是准确的限制获取的个数,而 count 只是给 redis 服务器一个“提示/建议”,写入的 count 和实际返回的 key 的个数不一定是完全相同的,但是不会差很多。

示例:

返回值的第一部分表示下次光标的位置,为0表示结束,第二部分是此次渐进式遍历获取到的 key 。

渐进式遍历在遍历过程中,不会在服务器存储人任何状态信息,此时的遍历是随时可以终止的,不会对服务器产生任何的副作用。

渐进式遍历 scan 虽然解决了阻塞问题,但是如果在遍历期间键有所变化(增加、删除、修改),可能导致遍历键的重复遍历或者遗漏。


🎅🏻 二、过期策略以及单线程模型

2.1 过期策略

在前面介绍过的 expire 命令中(什么?没有印象了?赶紧回去复习一下下),会发现当设置了过期时间以后,超过过期时间再查 key ,发现这个 key 被删除了。

本小节就来介绍一下,redis 的 key 的过期策略是什么实现的。

一个 redis 中可能同时存在很多很多 key,这些 key 可能有很大一部分都有过期时间,此时,redis 服务器怎么知道哪些 key 已经过期要删除?哪些 key 还没过期?

如果直接遍历所有的 key ,显然效率非常低,是不可取行为。

redis 整体的策略是:

1)定期删除

每隔一定时间扫描一定数量的数据库的expires字典,抽取其中一部分key进行验证过期时间,并清除其中已经过期的key,需要保证这个抽取的时间要足够快。

这里对于定期删除的时间有明确要求,因为 redis 是单线程的程序,主要的任务是处理客户端的命令,如果扫描过期key的时间太长了,就可能导致正常处理命令请求就阻塞了,也就是产生了类似于 key * 这样的效果。

2)惰性删除

假设一个key已经到过期时间了,但是暂时还没有删除这个 key ,key 还存在,当后面访问正好用到了,此时这次访问就会让 redis 服务器触发删除 key 的操作,同时返回一个 nil。

redis 同时使用了这两种过期策略:

每隔100ms 就随机抽取一定数量的 key 来检查和删除。

在获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。

具体内容可以查看 Redis 官方文档对于过期 key 给出的解释:

EXPIRE | Redis


2.2 单线程模型

本小节主要介绍 redis 的单线程模型。

redis 只使用一个线程,处理所有的命令请求。这里并不是说 redis 服务器进程内部只有一个线程,其实也有多个线程,只是多个线程都是在处理网络IO。

假设有当前场景:

两个 redis 客户端,同时针对 redis 服务器中的同一个 key 值进行操作,此时,在多线程下可能会发生线程安全问题。

博主之前的博客介绍过线程安全问题:

线程安全问题-CSDN博客

幸运的是,虽然上述客户端请求是“并发”的,但是 redis 服务器由于是单线程模型,保证了收到的这多个请求是串行执行的。即使多个请求同时到达 redis 服务器,也是要先在队列中进行排队的,等待 redis 服务器从队列中逐个取出里面的命令再执行,从微观上将,redis 服务器是串行执行这多个命令的。

举例来说:这就相当于是学生时代每逢周内中午开饭,当开饭铃声(下课铃声)响起时,干饭人大军浩浩荡荡冲向食堂,假设只有一个食堂且只有一个窗口,此时,从宏观上看,一群人同时到达食堂(并发执行),微观上,到了食堂以后,需要在窗口前进行排队,排到了才进行打饭(串行执行)

redis 能够使用单线程模型很好工作的原因,主要是因为:redis 的核心业务逻辑都是短平快的,不太消耗 cpu 资源也就无需多核了。


2.3 Redis 效率为什么这么高

在前面的小节中,介绍了 redis 是单线程模型,那么,铁铁们是否有这样的疑问

既然 redis 是单线程模型,为什么相较于数据库(MySQL),它的执行效率如此之快?

究其原因,主要有以下几点:

1. redis 访问内存,数据库访问硬盘。

2. redis 的核心功能比数据库的核心功能更简单。

    数据库对于数据的增删改查都有复杂的功能支持,这样的功能势必会花费更大的开销。比如:针对插入删除操作,数据库有各种约束,这些约束会导致数据库做额外的功能。而 redis干的活少, 提供的功能相较于MySQL等数据库少了很多。

3.单线程模型,避免了一些不必要的线程竞争开销。

   redis 的每个基本操作,都是短平快的,也就是简单操作一下内存数据,不是特别消耗 cpu 资源的操作,因此,即使搞多个线程也提升不大。

4. redis 处理网络 IO 的时候,使用了 epoll 这样的 IO 多路复用机制。

    通过 IO 多路复用机制,就可以使得一个线程管理多个 socket。

    针对 TCP 来说,每次要服务一个客户端,都需要给这个客户端安排一个 socket 。一个服务器服务多个客户端,同时有很多个 socket,但是,这些 socket 并非每时每刻都在传输数据,可能有些“摸鱼”的 socket(很多情况下,每个客户端和服务器之间的通信并不是很频繁,因此,不通信的 socket 都是出于静默状态,同一时刻,只有少数 socket 是活跃的)。

   对于 IO 多路复用,C++ 可以直接使用 Linux 原生的 epoll api,而 Java 可以使用 NIO (标准库提供的一组类,底层就是封装了 epoll)。

   举例简单解释一下 epoll:

    假设大四老学长小马今日要和寝室的两位室友买饭回寝室吃,室友1:简简单单一份蛋炒饭,室友2:热热的肉夹馍,小马:老马家的羊头。

    此时,想要解决带饭回寝室吃这个需求,有如下几种解决方案:

    第一种:小马决定跑腿,先去买蛋炒饭,等饭好了带上再去买肉夹馍,等肉夹馍好了带上去买老马家的羊头,等羊头好了以后,带回寝室一起吃饭。

    第二种: 三人一起去买饭,各自买各自的,然后带回寝室。

    第三种: 小马又决定跑腿,先去买蛋炒饭,等蛋炒饭的过程去买肉夹馍,买完肉夹馍马不停蹄奔向老马家的羊头。下单好了以后,小马找了个位置坐了下来,等待三家店的老板通知(帅哥,你的订单好了)。

    分析上述三种情况,第一种相当于一个线程执行操作,阻塞后等待执行,效率最低;第二种多个线程执行操作,效率非常高,但是系统开销大了;第三种,使用 IO 多路复用机制,一个线程执行多个客户端操作,效率非常高。

    对于第三种情况来说,能够高效完成这三件事的前提是:这三件事的交互并不频繁,大部分时间都在等,而例子中,老板通知(帅哥,你的订单好了)对应的就是 epoll 事件通知/回调机制。

    


🎅🏻 三、Java 客户端操作 Redis

本小结主要介绍Java语言的 Redis 客户端的使用方法。

首先来认识一下 redis 自定义的应用层协议:RESP

官网链接如下:Redis serialization protocol specification | Docs

这里翻译部分重点信息。 

redis 的请求-响应模型是一问一答的形式,也就是客户端给服务器发送一个请求,服务器返回一个响应。

 客户端给服务器发送的是 redis 命令(也就是前面介绍的若干命令),是以 bluk string 数组的形式发送的。

 服务器返回的响应针对不同的命令,返回结果不一样,有的命令,返回一个ok,有的命令可能返回整数,有的命令可能返回数组。

当了解了 redis 的 RESP 协议以后,就可以实现客户端程序了。

Java 生态中,封装 RESP 协议实现的 redis 的客户端是有很多的,小马这里使用的是 jedis,jedis 提供的 api 命令 和 redis 命令高度一致。

jedis 可以通过 maven (中央仓库)下载。创建一个 Maven 项目,引入依赖:

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.6</version>
</dependency>

 在编写代码之前,需要注意以下关键点:

由于是在 windows 系统上(idea)通过 redis 客户端来访问 redis 服务器(linux 云服务器),需要通过云服务器的外网 IP 来访问到 linux 服务器,如下图: 

这里只修改成外网 IP 还是不够的,6379 端口默认是被云服务器的防火墙给保护起来的,不能被外面访问到。但是,防火墙一保护,不光黑客,用户自己也访问不到,因此,可以通过在云服务器后台防火墙开放6379端口使得用户可以访问到,但是,当 Redis 的端口一旦公开到网上,就特别容易被入侵(小马就被折磨过很多个日夜)。

因此,是否有方法既不用开放 redis 端口,用户自己又能够通过外网访问。

答案是有的。 这里主要有两种方法:

1)直接让 java 程序也在 linux 上运行。

     此时,客户端和服务器在同一主机上,就不涉及跨主机访问,但是这就需要程序员自己将代码打成可执行的 jar 包,然后把 jar 包拷贝到 linux 服务器上执行。但是上述这种操作,手工实现比较麻烦,虽然可以通过一些第三方插件来简化上述步骤,但是实际操作还是比较麻烦。因此,更推荐第二种方法。

2)配置 ssh 端口转发(隧道、端口映射),把云服务器的 redis 端口映射到本地主机。

      在 windows 主机上,可以通过一些终端(xshell、finallshell...),远程登录到 linux 服务器,此时,终端登录到 linux 云服务上使用 ssh 协议(基于 tcp 的应用层协议),端口号默认是 22,ssh 功能非常强大,其中很重要的一个特性就是能够支持端口转发,相当于通过 ssh 的22端口,来传递其他端口数据。初衷是想通过 windows 主机访问云服务器的 6379 端口,此时就可以构造一个特殊的 ssh 数据报,把要访问的 redis 请求,放到 ssh 数据报里,如下图:

这个数据报就会通过 22 端口发送给 linux 服务器,服务器的 ssh 服务器程序就能够解析出上述的数据报,然后把数据报交给 6379 端口的程序。

由于一个 linux 主机上存在的服务器有很多,ssh 也可能需要给多个端口传递数据,为了区分不同的端口,往往会把服务器的端口在本地用另外一个端口进行表示。比如,想访问 linux 服务器上的 6379 端口,就可以借助 ssh 协议,将这个 6379 端口映射到 windows 主机上的端口(假设8888端口),如下图:

此时,客户端的程序如果访问 127.0.0.1:8888 就相当于是访问到 linux 服务器的 6379 端口了。

因此,只需要进行简单的配置,后续把云服务器的端口当成一个本地的端口使用即可。

具体配置操作如下(以 finalshell为例):

当 ssh 连接上了以后,端口转发才会生效(ssh 断开,端口转发失效),可以使用 netstat 命令查看本地 8888 端口是否被绑定。

 当断开 ssh 连接,会发现 8888 端口未被绑定:

需要注意:当配置了端口转发之后,要断开之前的连接,重新连接才能生效。

通过上述操作,在后续 Java 代码中,通过 127.0.0.1:8888 就能操作到云服务器的 redis 了,同时,外面的客户端是无法直接访问到云服务器的 6379 的。

经过上述铺垫,就可以开始紧张刺激的敲代码环节了。

 先写一个Demo验证:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {
    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        // 从 redis 连接池中取出一个连接,连接用完后需要释放
        // 此处的释放不一定是真的关闭 tcp 连接,而是放回到池子中
        // 这里使用 try with resource 语法进行资源释放
        try(Jedis jedis = jedisPool.getResource()) {
            String pong = jedis.ping();
            System.out.println(pong);
        }
    }
}

查看执行结果:

 说明程序成功运行。

这里还需要注意的一点是,除了配置 ssh 端口映射之外,要修改 redis.conf 中的配置,要配置绑定的ip以及关闭保护模式:

 接下来演示 redis 的一些通用命令:

get 和 set

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {

    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
           test1(jedis);
        }
    }

    private static void test1(Jedis jedis) {
        System.out.println("get 和 set 的使用");
        // 清空数据库,避免上一组残留的测试数据影响这一组的测试结果
        jedis.flushAll();
        jedis.set("key","111");
        jedis.set("key2","222");
        // 还可以设置过期时间
        SetParams setParams = new SetParams();
        // 指定10 s后过期
        setParams.ex(10);
        // 当key存在的时候进行修改
        setParams.xx();
        jedis.set("key","333",setParams);

        String value = jedis.get("key");
        System.out.println("value = "+value);
    }
}

exists 和 del

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {

    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
           test1(jedis);
        }
    }

    private static void test1(Jedis jedis) {
        System.out.println("exists 和 del 的使用");
        // 清空数据库
        jedis.flushAll();
        jedis.set("key1","111");
        jedis.set("key2","222");
        jedis.set("key3","333");
        boolean result = jedis.exists("key1");
        System.out.println("reslut = "+result);
        // 删除 key
        // del 支持删除多个 key
        long ret = jedis.del("key","key1","key2");
        System.out.println("ret = "+ret);
        result = jedis.exists("key3");
        System.out.println("reslut = "+result);
    }
}

keys 命令

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Set;


/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {

    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
           test1(jedis);
        }
    }

    private static void test1(Jedis jedis) {
        System.out.println("keys 的使用");
        // 清空数据库
        jedis.flushAll();
        jedis.set("key1","111");
        jedis.set("hello","world");
        SetParams params = new SetParams();
        params.ex(20);
        params.nx();
        jedis.set("three","00000",params);
        // 使用 keys 命令
        // redis 的 key 是不能重复的,并且顺序没有要求
        Set<String> set1 = jedis.keys("*");
        System.out.println("set1: "+set1);
        Set<String> set2 = jedis.keys("k?y1");
        System.out.println("set2: "+set2);
    }
}

expire 和  ttl

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Set;


/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {

    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
           test1(jedis);
        }
    }

    private static void test1(Jedis jedis) {
        System.out.println("expire 和 ttl 的使用");
        // 清空数据库
        jedis.flushAll();
        jedis.set("key1","value1");
        jedis.expire("key1",5);
        long ttl = jedis.ttl("key1");
        System.out.println("ttl: "+ttl);
        // 阻塞 3 秒
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ttl = jedis.ttl("key1");
        System.out.println("ttl: "+ttl);
    }
}

type 

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;


/**
 * @author 26568
 * @date 2024-05-04 20:03
 */
public class RedisDemo {

    public static void main(String[] args) {
        // 连接到 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
           test1(jedis);
        }
    }

    private static void test1(Jedis jedis) {
        System.out.println("expire 和 ttl 的使用");
        // 清空数据库
        jedis.flushAll();
        // String 类型
        jedis.set("key1","value1");
        String type = jedis.type("key1");
        System.out.println("type = "+type);
        // List 类型
        jedis.lpush("key2","111","222","333");
        type = jedis.type("key2");
        System.out.println("type = "+type);
        // Hash 类型
        Map<String,String> map = new HashMap<>();
        map.put("f1","hhh");
        map.put("f2","aaaa");
        jedis.hset("key3",map);
        type = jedis.type("key3");
        System.out.println("type = "+type);
        // 集合类型
        jedis.sadd("key4","zhangsan","lisi");
        type = jedis.type("key4");
        System.out.println("type = "+type);
        // 有序集合类型
        Map<String,Double> scoreMembers = new HashMap<>();
        scoreMembers.put("xiaoma",99.45);
        scoreMembers.put("libai",88.50);
        jedis.zadd("key5",scoreMembers);
        type = jedis.type("key5");
        System.out.println("type = "+type);
    }
}

接下来介绍一下 String 类型的一些命令:

1.get 和 set

2.mget 和 mset

3.getrange 和 setrange

4.append

5.incr 和 decr 

mget 和 mset

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:00
 */
public class RedisString {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("mget 和 mset");
        // 清空数据库
        jedis.flushAll();
        jedis.mset("key1","111","key2","222","key3","333");
        // 不存在的 key 会返回 null
        List<String> reslut = jedis.mget("key1","key2","key3","key4");
        System.out.println("result: "+reslut);
    }
}

getrange 和 setrange

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:00
 */
public class RedisString {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("getrange 和 setrange");
        // 清空数据库
        jedis.flushAll();
        jedis.set("key","abcdefghijklmn");
        // 获取范围内(闭区间)的字符串
        String reslut = jedis.getrange("key",3,6);
        System.out.println("result: "+reslut);
        // 从指定位置开始修改字符串
        jedis.setrange("key",0,"world");
        reslut = jedis.get("key");
        System.out.println("result: "+reslut);
    }
}

append

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:00
 */
public class RedisString {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("append");
        // 清空数据库
        jedis.flushAll();
       jedis.set("key","hello");
       // 拼接
        jedis.append("key","world");
        // 获取完整字符串
        String result = jedis.getrange("key",0,-1);
        System.out.println("resut: "+result);
    }
}

incr 和 decr 

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:00
 */
public class RedisString {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("incr 和 decr");
        // 清空数据库
        jedis.flushAll();
        jedis.set("key","100");
        // +1
        long result = jedis.incr("key");
        System.out.println("result: " + result);
        String value = jedis.get("key");
        System.out.println("value: " + value);
        // -1
        result = jedis.decr("key");
        System.out.println("result: " + result);
        value = jedis.get("key");
        System.out.println("value: " + value);
    }
}

List 类型的命令:

1.lpush 和 lrange

2.rpush 、rpop、lpop

3.blpop、brpop

4.llen

lpush 和 lrange

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:17
 */
public class RedisList {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("lpush 和 lrange");
        // 清空数据库
        jedis.flushAll();
        jedis.lpush("key","111","222","333","444");
        List<String> reslut = jedis.lrange("key",0,2);
        System.out.println("result: "+reslut);
    }
}

rpush 、rpop、lpop

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:17
 */
public class RedisList {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("rpush 、rpop、lpop");
        // 清空数据库
        jedis.flushAll();
        jedis.rpush("key","111","222","333");
        List<String> result = jedis.lrange("key",0,-1);
        System.out.println("result: " + result);
        String value1 = jedis.rpop("key");
        System.out.println("value1: "+value1);
        String value2 = jedis.lpop("key");
        System.out.println("value2: "+value2);
    }
}

blpop、brpop

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:17
 */
public class RedisList {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("blpop 和 brpop");
        // 清空数据库
        jedis.flushAll();
        // 返回结果是一个二元组,一个是从哪个 key对应的list删除,一个是要删除的元素是什么
        List<String> results = jedis.blpop(3,"key");
        System.out.println("results[0]: "+results.get(0));
        System.out.println("results[1]: "+results.get(1));
    }
}

 这里报空指针异常是因为:3秒后,返回一个空list,因此会发生空指针异常。

设置阻塞时间久一点,在这个期间,往 key 中插入元素。

此时阻塞停止,返回结果:

brpop 和 blpop 操作几乎一样,这里不在赘述。 

llen

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.List;

/**
 * @author 26568
 * @date 2024-05-05 11:17
 */
public class RedisList {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("llen");
        // 清空数据库
        jedis.flushAll();
        jedis.lpush("key","111","222","333");
        long len = jedis.llen("key");
        System.out.println("len = "+len);
    }
}

接下来介绍 Set 类型的一些命令:

1.sadd 和 smembers

2.sismember

3.scard

4.spop

5.sinter 和 sinterstore

sadd 和 smembers

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:29
 */
public class RedisSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("sadd 和 smembers");
        // 清空当前数据库
        jedis.flushDB();
        // 不会添加重复元素
        long result = jedis.sadd("key","zhangsan","lisi","wangwu","wangwu");
        System.out.println("result = "+result);
        Set<String> list = jedis.smembers("key");
        System.out.println("list: "+list);
    }
}

sismember

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:29
 */
public class RedisSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("sismember");
        // 清空当前数据库
        jedis.flushDB();
        // 不会添加重复元素
        long result = jedis.sadd("key","zhangsan","lisi","wangwu","wangwu");
        System.out.println("result = "+result);
        boolean flag = jedis.sismember("key","zhangsan");
        System.out.println("flag: "+flag);
        flag = jedis.sismember("key","hhhhhh");
        System.out.println("flag: "+ flag);
    }
}

scard

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:29
 */
public class RedisSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("scard");
        // 清空当前数据库
        jedis.flushDB();
        // 不会添加重复元素
        long result = jedis.sadd("key","zhangsan","lisi","wangwu","wangwu");
        System.out.println("result = "+result);
        long len = jedis.scard("key");
        System.out.println("len: "+len);
    }
}

spop

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:29
 */
public class RedisSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("spop");
        // 清空当前数据库
        jedis.flushDB();
        // 不会添加重复元素
        long count = jedis.sadd("key","zhangsan","lisi","wangwu","wangwu");
        System.out.println("result = "+count);
        // 这里随机取出 member
        String result = jedis.spop("key");
        System.out.println("result: "+result);
    }
}

sinter 和 sinterstore

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:29
 */
public class RedisSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("sinter 和 sinterstore");
        // 清空当前数据库
        jedis.flushDB();
        // 不会添加重复元素
        jedis.sadd("key1","zhangsan","lisi","wangwu","wangwu");
        jedis.sadd("key2","wangwu","zhangsan","xiaoma");
        Set<String> results = jedis.sinter("key1","key2");
        System.out.println("results: "+results);
        long len = jedis.sinterstore("key3","key1","key2");
        System.out.println("len: "+len);
        Set<String> members = jedis.smembers("key3");
        System.out.println("members:" +members);
    }
}

接下来介绍 Hash 类型:

1.hset 和 hget

2.hexists 和 hdel

3.hkeys 和 hvals

4.hmget 和 hmset

hset 和 hget

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 26568
 * @date 2024-05-05 19:48
 */
public class RedisHash {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("hset 和 hget");
        // 清空数据库
        jedis.flushAll();
        jedis.hset("key","f1","111");
        // 还可以通过map添加
        Map<String,String> hash = new HashMap<>();
        hash.put("f2","222");
        jedis.hset("key",hash);
        String value = jedis.hget("key","f1");
        System.out.println("value = "+value);
    }
}

hexists 和 hdel

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 26568
 * @date 2024-05-05 19:48
 */
public class RedisHash {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("hexists 和 hdel");
        // 清空数据库
        jedis.flushAll();
        jedis.hset("key","f1","111");
        boolean result = jedis.hexists("key","f1");
        System.out.println("result: "+result);
        long len = jedis.hdel("key","f1","f2");
        System.out.println("len : "+len);
    }
}

hkeys 和 hvals

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:48
 */
public class RedisHash {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("hkeys 和 hvals");
        // 清空数据库
        jedis.flushAll();
        jedis.hset("key","f1","111");
        jedis.hset("key","f2","222");
        Set<String> fields = jedis.hkeys("key");
        List<String> vals = jedis.hvals("key");
        System.out.println("fields: "+fields);
        System.out.println("vals: "+vals);

    }
}

hmget 和 hmset

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author 26568
 * @date 2024-05-05 19:48
 */
public class RedisHash {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("hmset 和 hmget");
        // 清空数据库
        jedis.flushAll();
        Map<String,String> hash = new HashMap<>();
        hash.put("f1","111");
        hash.put("f2","222");
        // 只能传map
        jedis.hmset("key",hash);
        List<String> results = jedis.hmget("key","f1","f2");
        System.out.println("results: "+results);
    }
}

 最后介绍一下 Zset 类型的一些命令:

1.zadd 和 zrange

2.zcard 和 zscore

3.zrem 和 zrank

zadd 和 zrange

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.resps.Tuple;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 26568
 * @date 2024-05-05 20:09
 */
public class RedisZSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("zadd 和 zrange");
        // 清空数据库
        jedis.flushAll();
        jedis.zadd("key",88.5,"zhangsan");
        // 也可以使用 map
        Map<String,Double> map = new HashMap<>();
        map.put("lisi",65.2);
        map.put("zhaoliu",46.6);
        jedis.zadd("key",map);
        List<String> members = jedis.zrange("key",0,-1);
        System.out.println("members: "+members);
        List<Tuple> memberWithScores = jedis.zrangeWithScores("key",0,2);
        System.out.println("memberWithScores: "+memberWithScores);
        // 获取单个元素
        String member = memberWithScores.get(0).getElement();
        double score = memberWithScores.get(0).getScore();
        System.out.println("member: "+member+", score"+score);

    }
}

zcard 和 zscore

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.resps.Tuple;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 26568
 * @date 2024-05-05 20:09
 */
public class RedisZSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("zcard 和 zscore");
        // 清空数据库
        jedis.flushAll();
        jedis.zadd("key",88.5,"zhangsan");
        // 也可以使用 map
        Map<String,Double> map = new HashMap<>();
        map.put("lisi",65.2);
        map.put("zhaoliu",46.6);
        jedis.zadd("key",map);
        long len = jedis.zcard("key");
        System.out.println("len = "+len);
        Double score = jedis.zscore("key","zhangsan");
        System.out.println("score = "+score);
    }
}

zrem 和 zrank

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.resps.Tuple;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 26568
 * @date 2024-05-05 20:09
 */
public class RedisZSet {
    public static void main(String[] args) {
        // 连接 redis 服务器
        JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
        try (Jedis jedis = jedisPool.getResource()) {
            test(jedis);
        }
    }

    private static void test(Jedis jedis) {
        System.out.println("zrem 和 zrank");
        // 清空数据库
        jedis.flushAll();
        jedis.zadd("key",88.5,"zhangsan");
        // 也可以使用 map
        Map<String,Double> map = new HashMap<>();
        map.put("lisi",65.2);
        map.put("zhaoliu",46.6);
        jedis.zadd("key",map);
        long n = jedis.zrem("key","zhangsan");
        System.out.println("n = "+n);
        List<Tuple> memberWithScores = jedis.zrangeWithScores("key",0,-1);
        System.out.println("memberWithScores: "+memberWithScores);
        // 使用包装类型接收,如果不存在 返回 null,null 无法转成 long 基本数据类型
        Long index = jedis.zrank("key","zhaoliu");
        System.out.println("index = "+index);
    }
}

 以上就是 Java 客户端操作 Redis 的知识点介绍,关于命令只介绍了一些比较重要的命令,还有很多命令没有涉及到,铁铁们可以自己多多尝试 。


🎅🏻 四、Spring 操作 Redis

接下来就是本篇最后一节。

先创一个 Spring 项目:

这里需要注意,添加如下依赖:

在配置文件中添加如下配置:

 创建一个 Controller 类,如下:

Spring 是通过StringRedisTemplate 来操作 redis。

最原始的提供的类是 RedisTemplate,StringRedisTemplate 是 RedisTemplate 的子类,专门用来处理文本数据的,而 RedisTemplate 既可以处理二进制数据又可以处理文本数据,ByteArrayRedisTemplate 用来处理二进制数据。

StringTemplate 这个类提供的方法,相比于之前的 Jedis 中的各种方法,存在比较大的差异。主要是通过如下一些方法获得操作 Reids 基本数据类型的对象:

当获取到这些对象以后,就可以使用这些对象提供的方法操作 redis。

 String 类型命令:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试 redis 的各种方法,都通过这个 Controller 提供的 http 接口来触发
 * @author 26568
 * @date 2024-05-06 19:58
 */
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testString")
    public String testString() {
        redisTemplate.opsForValue().set("key1","111");
        redisTemplate.opsForValue().set("key2","222");
        redisTemplate.opsForValue().set("key3","333");
        String value = redisTemplate.opsForValue().get("key2");
        System.out.println("value = "+value);
        return "OK,String";
    }

}

输入网址:

查看控制台打印信息:

List 类型命令:

提供的命令如下:

在之前使用 Jedis 操作各个类型时,第一步操作是清除数据库,RdisTemplate 主要是通过提供的execute 方法来执行 Redis 的原生命令,此时就可以执行清除数据库操作。

execute 的方法是一个函数式接口,相当于一个回调函数,在回调里写要执行的 redis 的命令,这个回调就会被 RedisTemplate 内部进行执行。

这里的 RedisConnection 就代表了 Redis 连接,对标Jedis 对象。

代码如下:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试 redis 的各种方法,都通过这个 Controller 提供的 http 接口来触发
 * @author 26568
 * @date 2024-05-06 19:58
 */
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testList")
    public String testString() {
        // 清除数据库
        // 此处使用 lambda 表达式
        redisTemplate.execute((RedisConnection connection) -> {
            // execute 要求回调方法中必须写 return 语句
            // 这个回调返回的对象,就会作为 execute 本身的返回值
            // 通过connection 就可以执行 redis 的一些原生命令(类似 Jedis)
            connection.flushAll();
            return null;
        });
        redisTemplate.opsForList().leftPush("key","111");
        redisTemplate.opsForList().leftPush("key","222");
        redisTemplate.opsForList().leftPush("key","333");
        String value = redisTemplate.opsForList().rightPop("key");
        System.out.println("value = "+value);
        return "OK,List";
    }

}

输入网址:

 查看控制台打印信息:

 Set 类型命令:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Set;

/**
 * 测试 redis 的各种方法,都通过这个 Controller 提供的 http 接口来触发
 * @author 26568
 * @date 2024-05-06 19:58
 */
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testSet")
    public String testString() {
        // 清空数据库
        redisTemplate.execute((RedisConnection connection) -> {
            connection.flushAll();
            return null;
        });
        // 添加元素 相当于 sadd 命令
        redisTemplate.opsForSet().add("key","111","222","333");
        // 获取所有 member 相当于 smembers 命令
        Set<String> results = redisTemplate.opsForSet().members("key");
        System.out.println("results: "+results);
        // 判断在 key 中是否有member  相当于sismember 命令 
        Boolean exists = redisTemplate.opsForSet().isMember("key","111");
        System.out.println("exists: "+exists);
        // Set 中 member 的个数  相当于 scard命令
        Long count = redisTemplate.opsForSet().size("key");
        System.out.println("count: "+count);
        // 删除 member 相当于srem 命令
        redisTemplate.opsForSet().remove("key","111","333");
        results = redisTemplate.opsForSet().members("key");
        System.out.println("results: "+results);
        return "OK,Set";
    }

}

输入网址:

查看控制台打印信息:

Hash 类型:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Set;

/**
 * 测试 redis 的各种方法,都通过这个 Controller 提供的 http 接口来触发
 * @author 26568
 * @date 2024-05-06 19:58
 */
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testHash")
    public String testString() {
        // 清空数据库
        redisTemplate.execute((RedisConnection connection) -> {
            connection.flushAll();
            return null;
        });
        // 添加元素 相当于 hset 命令
        redisTemplate.opsForHash().put("key","f1","111");
        redisTemplate.opsForHash().put("key","f2","222");
        redisTemplate.opsForHash().put("key","f3","333");
        // 获取值(返回的是 Object 类型,需要转换)  相当于 hget 命令
        String value = (String) redisTemplate.opsForHash().get("key","f2");
        System.out.println("value = "+value);
        // 判断field是否存在  相当于 hexists
        Boolean exists = redisTemplate.opsForHash().hasKey("key","f3");
        System.out.println("exists: "+exists);
        // 获取field 个数  相当于 hlen
        Long count = redisTemplate.opsForHash().size("key");
        System.out.println("count = "+count);
        // 删除 field   相当于 hdel
        redisTemplate.opsForHash().delete("key","f1","f2");
        count = redisTemplate.opsForHash().size("key");
        System.out.println("count = "+count);
        return "OK,Hash";
    }

}

输入网址:

查看控制台打印信息:

 

 ZSet 类型:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Set;

/**
 * 测试 redis 的各种方法,都通过这个 Controller 提供的 http 接口来触发
 * @author 26568
 * @date 2024-05-06 19:58
 */
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testZSet")
    public String testString() {
        // 清空数据库
       redisTemplate.execute((RedisConnection connection) -> {
           connection.flushAll();
           return null;
       });
       // 添加元素 相当于 zadd 命令
       redisTemplate.opsForZSet().add("key","zhangsan",88.1);
       redisTemplate.opsForZSet().add("key","lisi",77.5);
       redisTemplate.opsForZSet().add("key","wangwu",65.41);
       // 获取区间内的 member     相当于 zrange 命令
       Set<String> members = redisTemplate.opsForZSet().range("key",0,-1);
       System.out.println("members: "+members);
       // 获取区间内的 member 包括分数  相当于 zrange key stop stop withscores
       Set<ZSetOperations.TypedTuple<String>> memberWithScores = redisTemplate.opsForZSet().rangeWithScores("key",0,-1);
       System.out.println("memberWithScores: "+memberWithScores);
       // 根据 member 获取 score  相当于 zscore 命令
       Double score = redisTemplate.opsForZSet().score("key","lisi");
       System.out.println("score = "+score);
       // 删除 member 相当于 zrem 命令
       redisTemplate.opsForZSet().remove("key","zhangsan");
       // 获取 member 个数  相当于 zcard 命令
       Long size = redisTemplate.opsForZSet().size("key");
       System.out.println("size: "+size);
       // 获取 member 下标 相当于 zrank 命令
       Long rank = redisTemplate.opsForZSet().rank("key","lisi");
       System.out.println("rank = "+rank);
       return "OK,ZSet";
    }

}

输入网址:

查看控制台打印信息;

以上就是 Spring 操作 Redis 的简单命令介绍。

详细的命令可以查看官网:

 https://github.com/spring-projects/spring-data-redis


 以上就是本篇博客的所有内容,如有帮助,倍感欣喜,如有错误,烦请指正。

 Redis 三部曲的最后一"曲"敬请期待(疯狂写作ing)

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/596718.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI人才争夺战,华尔街入局:豪掷百万美元年薪抢人 | 最新快讯

量子位公众号 QbitAI 继硅谷之后&#xff0c;华尔街也入局“AI 人才争夺大战”。 他们的目标非常明确——抢的就是高精尖的 AI 专家。 △图源&#xff1a;Business Insider 现在这条“街”上&#xff0c;不论是银行、对冲基金还是私募股权公司都已纷纷下场&#xff0c;可谓是豪…

(读书笔记-大模型) LLM Powered Autonomous Agents

目录 智能体系统的概念 规划组件 记忆组件 工具组件 案例研究 智能体系统的概念 在大语言模型&#xff08;LLM&#xff09;赋能的自主智能体系统中&#xff0c;LLM 充当了智能体的大脑&#xff0c;其三个关键组件分别如下&#xff1a; 首先是规划&#xff0c;它又分为以下…

2024第六届人工智能与教育国际研讨会(WAIE 2024)即将召开!

2024第六届人工智能与教育国际研讨会&#xff08;WAIE 2024&#xff09;将于2024年9月28-30日在日本东京举行。WAIE 2024的召开&#xff0c;旨在汇聚全球智慧&#xff0c;共同探讨人工智能在教育领域的应用与发展&#xff0c;找到人工智能与教育融合发展的最佳路径&#xff0c;…

从零开始的软件测试学习之旅(五)web测试项目

这里写目录标题 功能型测试非功能性测试面试拓展项目与数据库关系 测试用例设计—基于TPshop前台下单流程 功能型测试 一.设计测试 a,需求分析 1.输入分析 分析项目中要求如:输入长度,类型要求,组成规则,是否为空,是否重复 2.交付分析 判断所有数据正确,有错误给出提示(优化…

i.MX 6ULL 裸机 IAR 环境安装

一. IAR 的安装请自行搜索 二. 使用最新版本的 IAR&#xff0c;需要修改 SDK 1. 在 SDK 的 core_ca7.h 加上 #include "intrinsics.h" /* IAR Intrinsics */ 2. debug 时需要修改每个工程下的 ddr_init.jlinkscript&#xff0c;参考链接 Solved: How to conn…

双重检验锁方式实现单例模式

单例模式&#xff08;Singleton Pattern&#xff09;&#xff1a;是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时&#xff0c;为了防止频繁地创建对象使得内存飙升&#xff0c;单例模式可以让程序仅在内存中创建一个对象&#xff0c…

电源小白入门学习7——USB充电、供电、电源路径管理

电源小白入门学习7——USB充电、供电、电源路径管理 USB充电系统需要考虑的因素开关充电和线性充电充电路径管理输入限流路径管理&#xff08;动态功率管理&#xff09;理想二极管帮助提高电池利用率输入过充抑制 上期我们介绍了锂离子电池的电池特性&#xff0c;及充电电路设计…

OpenNJet评测,探寻云原生之美

在信息时代的大海上&#xff0c;云原生应用引擎如一艘航行于波涛之间的帆船&#xff0c;承载着创新的梦想和数字化的未来。本文将带领您登上这艘船&#xff0c;聚焦其中之一的OpenNJet&#xff0c;一同探寻其中的奥秘和精妙&#xff0c;领略其独特之美。 OpenNJet 内容浅析 O…

【JavaScript】数据类型转换

JavaScript 中的数据类型转换主要包括两种&#xff1a;隐式类型转换&#xff08;Implicit Type Conversion&#xff09;和显式类型转换&#xff08;Explicit Type Conversion&#xff09;。 1. 隐式类型转换&#xff08;自动转换&#xff09;&#xff1a; js 是动态语言&…

代码随想录第51天 | 309.最佳买卖股票时机含冷冻期

309.最佳买卖股票时机含冷冻期 309. 买卖股票的最佳时机含冷冻期 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划来决定最佳时机&#xff0c;这次有冷冻期&#xff01;| LeetCode&#xff1a;309.买卖股票的最佳时机含冷冻期_哔哩哔哩_bi…

ncnn 算子操作描述

ncnn 算子操作描述&#xff0c;具体查询见 ncnn/docs/developer-guide/operators.md at master Tencent/ncnn GitHub 都是从上述地方copy过来的&#xff0c;做备份。 具体如下&#xff1a; 1.AbsVal: 计算输入张量中的每个元素的绝对值。 y abs(x)one_blob_only 只支持…

Go 语言(四)【常用包使用】

1、命令行参数包 flag flag 包就是一个用来解析命令行参数的工具。 1.1、os.Args import ("fmt""os" )func main() {if len(os.Args) > 0 {for index, arg : range os.Args {fmt.Printf("args[%d]%v\n", index, arg)}} } 运行结果&#…

【Docker】docker部署lnmp和搭建wordpress网站

环境准备 docker&#xff1a;192.168.67.30 虚拟机&#xff1a;4核4G systemctl stop firewalld systemctl disable firewalld setenforce 0 安装docker #安装依赖包 yum -y install yum-utils device-mapper-persistent-data lvm2 #设置阿里云镜像 yum-config-manager --add…

MTEB - Embedding 模型排行榜

文章目录 关于 MTEBMTEB 任务和数据集概览使用 MTEB Pythont 库Installation使用 关于 MTEB MTEB : Massive Text Embedding Benchmark github : https://github.com/embeddings-benchmark/mtebhuggingface : https://huggingface.co/spaces/mteb/leaderboardpaper : https:/…

全国31省对外开放程度、经济发展水平、ZF干预程度指标数据(2000-2022年)

01、数据介绍 自2000年至2022年&#xff0c;中国的对外开放程度不断深化、经济发展水平不断提高、ZF不断探索并调整自身在经济运行中的角色和定位&#xff0c;以更好地适应国内外环境的变化&#xff0c;也取得了举世瞩目的成就。这一期间&#xff0c;中国积极融入全球经济体系…

书籍推荐|经典书籍ic书籍REUSE METHODOLOGY MANUALFOR等和verilog网站推荐(附下载)

大家好&#xff0c;今天是51过后的第一个工作日&#xff0c;想必大家都还没有完全从节假日的吃喝玩乐模式转变为勤勤恳恳的打工人模式&#xff0c;当然也包括我&#xff0c;因此这次更新主要是分享几篇书籍和verilog相关的学习网站~ 首先是一本数字电路相关的基础书籍&#xf…

JavaScript 中的 Class 类

&#x1f525; 引言 在ECMAScript 2015&#xff08;ES6&#xff09;中&#xff0c;class 关键字被引入&#xff0c;为JavaScript带来了一种更接近传统面向对象语言的语法糖。类是创建对象的模板&#xff0c;它们封装了数据&#xff08;属性&#xff09;和行为&#xff08;方法&…

【SpringMVC 】什么是SpringMVC(一)?如何创建一个简单的springMvc应用?

文章目录 SpringMVC第一章1、什么是SpringMVC2、创建第一个SpringMVC的应用1-3步第4步第5步第6步7-8步3、基本语法1、进入控制器类的方式方式1:方式2:方式3:方式4:方式5:2、在控制器类中取值的方式方式1:方式2:方式3:方式4:方式5:方式6:超链接方式7:日期方式8:aja…

第一天学习(GPT)

1.图片和语义是如何映射的&#xff1f; **Dalle2&#xff1a;**首先会对图片和语义进行预训练&#xff0c;将二者向量存储起来&#xff0c;然后将语义的vector向量转成图片的向量&#xff0c;然后基于这个图片往回反向映射&#xff08;Diffusion&#xff09;——>根据这段描…

云原生周刊:Terraform 1.8 发布 | 2024.5.6

开源项目推荐 xlskubectl 用于控制 Kubernetes 集群的电子表格。xlskubectl 将 Google Spreadsheet 与 Kubernetes 集成。你可以通过用于跟踪费用的同一电子表格来管理集群。 git-sync git-sync 是一个简单的命令&#xff0c;它将 git 存储库拉入本地目录&#xff0c;等待一…
最新文章