【Redis】Redis 客户端开发与 Java 集成:RESP协议解析与实战操作


客⼾端

要了解 Redis 服务端和客⼾端的通信协议,以及 Java 语⾔的 Redis 客⼾端使⽤⽅法。

为什么我们能编写出一个自定义的 redis 客户端?

网络通信中会用到很多协议:应用层、传输层、网络层、数据链路层、物理层。除了应用层外,其他四层的协议是固定好的,是在系统内核或者驱动程序中实现的,我们只能选择不能修改。

而对于应用层,虽然业界有很多成熟的应用层协议,如 HTTP 等等。但是此处更多的时候,都会“自定义”应用层协议,Redis 此处的应用层协议就是自定义协议(当然,传输层还是基于 TCP)。

  1. 客户端按照这里的应用层协议,发送请求
  2. 服务器按照这个协议进行解析
  3. 再按照这个协议构造响应
  4. 客户端再解析这个响应

之所以能够通信成功,是因为开发客户端的人和开发服务器的人都清楚协议的细节。我们第三方想要开发 redis 客户端,也就需要知道 Redis 的应用层协议,而这个官方开放出来的,RESP portocal spec,Redis serialization protocal (RESP) specification,RESP 是 Redis 自定义的应用层协议的名字,而不是 response 的这个 resp。

RESP 协议

Redis 序列化协议(RESP)是客户端实现的一种用于与Redis服务器通信的协议。虽然该协议专门设计用于 Redis,但您也可以将其用于其他客户端-服务器软件项目。

优点:

  1. 简单好实现
  2. 快速进行解析
  3. 肉眼可读

RESP可以序列化不同的数据类型,包括整数、字符串和数组。它还具有特定于错误的类型。客户端将请求发送到 Redis 服务器,请求是一个字符串数组。数组的内容是服务器应执行的命令及其参数。服务器的响应类型是特定于命令的。

RESP 是二进制安全的,并使用前缀长度来传输批量数据,因此不需要处理从一个进程传输到另一个进程的批量数据。

客户端通过创建到 Redis 服务器端口的 TCP 连接来连接到 Redis 服务器(默认端口为6379)。虽然 RESP 在技术上与 TCP 无关,但在 Redis 的上下文中,该协议仅与 TCP 连接(或类似流式连接的 Unix 套接字)一起使用。

即传输层还是基于 TCP,但和 TCP 没有那么强耦合

Redis 服务器接受由不同参数组成的命令请求。服务器处理命令并将响应发送回客户端。

即请求和响应之间的通信模型是一问一答(一个请求一个响应)的形式

RESP 本质上是一种支持多种数据类型的序列化协议。在 RESP 中,数据的第一个字节确定其类型。在 RESP 序列化的有效负载中,第一个字节始终标识其类型。随后的字节构成类型的内容。

客户端给服务器发送的是 Redis 命令(bulk string、数组,这两种形式发送的)
服务器用 RESP 类型进行响应。响应的类型由命令的实现和可能的客户端协议版本确定

bulk string 可以传输二进制数据,simple string 只能传输文本

Redis 客户端服务器,要做的工作是:

  1. 按照对应格式,构造出字符串,往 socket 中写
  2. 从 socket 中读取字符串,按照对应格式解析

写代码就一定要按照协议解析/构造字符串吗?

不用,因为协议公开已久,有很多现成的库可以使用

Redis Java使⽤ 样例列表

引⼊依赖

Java 操作 redis 的客⼾端有很多. 其中最知名的是 jedis(这里提供的 api 和 Redis 命令是高度一致的).

创建 maven 项⽬, 把 jedis 的依赖拷⻉到 pom.xml 中.

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

版本选择⼀个相对较新的版本即可

配置端⼝转发

Redis 服务器安装在云服务器上, ⽽我们编写的代码则是在本地主机.

要想让本地主机能访问 redis, 需要把 redis 的端⼝通过云服务器后台⻚⾯的 “防⽕墙” / “安全组” 放开端⼝到公⽹上. 但是这个操作⾮常危险(⿊客会顺着 redis 端⼝进来).

因此我们可以使⽤端⼝转发的⽅式, 直接把服务器的 redis 端⼝映射到本地.

也可以通过直接让 Java 程序在 Linux 上运行的方式,即把代码打包成可执行的 jar 包,拷贝到 Linux 服务器上执行,但这种方式比较繁琐,因此也不常用

在 Xshell 中, 进⾏如下配置:

  1. 右键云服务器的会话, 选择属性.
  2. 找到隧道 -> 配置转移规则.

SSH 功能很强大,比较重要的特性就是能支持端口转发,相当于通过 SSH 的 22 端口来传递其他端口的数据。

比如我们本身是需要通过 Windows 主机,访问云服务器的 6379 端口,于是就构造一个特殊的 SSH 数据报,把要访问 Redis 的请求放大 SSH 数据报里,这个数据报就会通过 22 端口发送给服务器,服务器的 SSH 服务器程序就能解析出上述的数据报,然后把数据报交给 6379 端口的程序。

想在主机访问云服务器的 6379 端口,我们本地通过 SSH 弄一个端口,比如 8888,这是 SSH 程序监听的端口,然后把云服务器的 6379 映射到本地的 8888,此时客户端的程序访问 127.0.0.1:8888 就相当于访问 Linux 服务器的 6379

  1. 使⽤该会话连接服务器

在 Windows 本地计算机上可能就没有 Xshell 这些方法了,直接用一条命令或许就可以了

在本地的命令提示符窗口(通过 cmd 进入)后输入下面的命令

ssh -L 本地端口:localhost:6379 用户名@服务器IP地址
ssh -L 9999:localhost:6379 root@123.207.247.132
C:\Users\幽琴健>ssh -L 9999:localhost:6379 root@123.207.247.132
The authenticity of host '123.207.247.132 (123.207.247.132)' can't be established.
ED25519 key fingerprint is SHA256:lhr4h6nFEGM1h9sB14BWdZ/RoEUOInm2Ms9yuWJ/pg4.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '123.207.247.132' (ED25519) to the list of known hosts.
root@123.207.247.132's password:
Last failed login: Sat Apr  6 00:20:41 CST 2024 from 170.64.185.216 on ssh:notty
There were 269 failed login attempts since the last successful login.
Last login: Fri Apr  5 12:07:32 2024 from 127.0.0.1
[root@VM-8-16-centos ~]#

我这里的本地端口选择了 9999

连接是否生效,可以通过查看本地端口监听情况

在另一个窗口输入 netstat -ano | findstr 9999

C:\Users\幽琴健>netstat -ano | findstr 9999
TCP    127.0.0.1:9999         0.0.0.0:0              LISTENING       12312
TCP    [::1]:9999             [::]:0                 LISTENING       12312

C:\Users\幽琴健>

此时成功建立 SSH隧道 的窗口不要关闭,创建了一个从本地端口 9999 到远程服务器上的 Redis 服务端口 6379 的 SSH隧道。,在 IDEA 上执行代码发现就成功了

public class RedisDemo {
public static void main(String[] args) {
  // 连接到 Redis 服务器上
  JedisPool jedisPool=new JedisPool("tcp://127.0.0.1:9999");

  // 从 Redis 连接池中取出一个连接,用完后记得释放,这里的释放不一定是关闭 TCP,而是放回到池子里
  try(Jedis jedis=jedisPool.getResource()){
      // Redis 的各种命令都对应到 jedis 对象的各种方法
      String pong=jedis.ping();
      System.out.println(pong);
  }
}
}

此时, 访问本地的 8888, 就相当于访问对应服务器的 6379

注意, Xshell 和服务器必须处在连接状态, 这样的映射才是有效的.

连接 Redis Server
  • 使⽤ JedisPool 描述 Redis 服务器的位置. 使⽤ url 来表⽰.
  • 使⽤ getResource 和服务器建⽴连接.
  • 连接使⽤完毕需要 close 关闭. 也可以直接使⽤ try ⾃动关闭.
  • 通过 ping ⽅法可以检测连接是否正确建⽴.
public static void main(String[] args) {
    // 连接到 Redis 服务器上,这里只是开发阶段这么写,后续要部署时就要改成云服务器的实际情况
    JedisPool jedisPool=new JedisPool("tcp://127.0.0.1:9999");

    // 从 Redis 连接池中取出一个连接,用完后记得释放,这里的释放不一定是关闭 TCP,而是放回到池子里
    try(Jedis jedis=jedisPool.getResource()){
        // Redis 的各种命令都对应到 jedis 对象的各种方法
        String pong=jedis.ping();
        System.out.println(pong);
    }
}

执⾏结果

PONG

注意:

这个报错没有关系, 不影响使⽤, 忽略即可.

此时程序能跑通除了配置 SSH 端口映射之外,还跟最开始安装 Redis 服务器时,要配置绑定的 ip 以及关闭保护模式有关

bind 0.0.0.0

如果这里还是 127.0.0.1,那么只能本机和本机访问,不能跨主机访问

protected-mode no

如果这里还是 yes,开启保护模式,也不能跨主机访问

基础操作
  1. get / set
  2. exists
  3. del
  4. keys
  5. expire / ttl
  6. type

set 和 get

  • key 不存在时, 得到的 value 为 null
public static void main(String[] args) {
    JedisPool jedisPool = new JedisPool("tcp://127.0.0.1:8888");
    try (Jedis jedis = jedisPool.getResource()) {
        // testPing(jedis);
        testGetSet(jedis);
        jedis.flushDB();
    }
}

private static void testGetSet(Jedis jedis) {
    jedis.set("key1", "value1");
    jedis.set("key2", "value2");

    String value1 = jedis.get("key1");
    System.out.println(value1);

    String value2 = jedis.get("key2");
    System.out.println(value2);

    String valueNull = jedis.get("noSuchKey");
    System.out.println(valueNull);
}

执⾏结果

value1
value2
null

exists 和 del

  • del 可以删除多个 key, 以变⻓参数列表的⽅式体现. 返回值是实际删除的 key 的个数.
private static void testExistsAndDel(Jedis jedis) {
    jedis.set("key1", "value");
    jedis.set("key2", "value");
    jedis.set("key3", "value");
    
    boolean ret = jedis.exists("key1");
    System.out.println(ret);

    long n = jedis.del("key1");
    System.out.println(n);

    ret = jedis.exists("key1");
    System.out.println(ret);

    n = jedis.del("key2", "key3");
    System.out.println(n);
    ret = jedis.exists("key2");
    System.out.println(ret);
}

执行结果

true
1
false
2
false

keys

private static void testKeys(Jedis jedis) {
    jedis.set("key1", "value1");
    jedis.set("key2", "value2");
    jedis.set("key3", "value3");
    jedis.set("myKey", "value4");

    Set<String> keys = jedis.keys("*");
    System.out.println(keys);

    keys = jedis.keys("key?");
    System.out.println(keys);
}

这里用 Set 接收是因为,Redis 的 key 是不能重复的,而且也不在意顺序

执行结果

[key1, key2, key3, myKey]
[key1, key2, key3]

expire 和 ttl

private static void testExpireAndTTL(Jedis jedis) {
    jedis.setex("key", 60, "value");
    long ttl = jedis.ttl("key");
    System.out.println(ttl);
}

执行结果

60

type

private static void testType(Jedis jedis) {
    jedis.set("key1", "value");
    System.out.println(jedis.type("key1"));

    jedis.lpush("key2", "a", "b", "c");
    System.out.println(jedis.type("key2"));

    jedis.hset("key3", "name", "zhangsan");
    System.out.println(jedis.type("key3"));

    jedis.sadd("key4", "111", "222", "333");
    System.out.println(jedis.type("key4"));

    jedis.zadd("key5", 1, "aaa");
    System.out.println(jedis.type("key5"));
}

执行结果

string
list
hash
set
zset
字符串操作
  1. get / set
  2. mget / mset
  3. getrange / setrange
  4. append
  5. indr / decr

mgetmset

private static void testMSetAndMGet(Jedis jedis) {
    jedis.mset("key1", "value1", "key2", "value2", "key3", "value3");
    List<String> values = jedis.mget("key1", "key2", "key3");
    System.out.println(values);
}

执⾏结果

[value1, value2, value3]

如果是 jedis.mget("key1", "key2", "key100", "key3"); 这样的有个不存在的

最终的输出结果就是 [value1, value2, null, value3]

append

private static void testAppend(Jedis jedis) {
    jedis.append("key", "aaa");
    String value = jedis.get("key");
    System.out.println(value);
    jedis.append("key", "bbb");
    value = jedis.get("key");
    System.out.println(value);
}

执⾏结果

aaa
aaabbb

getrangesetrange

  • 注意 getrange 的区间是闭区间
private static void testGetRangeAndSetRange(Jedis jedis) {
    jedis.set("key", "abcdefg");
    String value = jedis.getrange("key", 1, 4);
    System.out.println(value);

    jedis.setrange("key", 0, "xyz");
    value = jedis.get("key");
    System.out.println(value);
}

执⾏结果

bcde
xyzdefg

setnx

private static void testSetnx(Jedis jedis) {
    long n = jedis.setnx("key", "value");
    System.out.println(n);
    String value = jedis.get("key");
    System.out.println(value);

    n = jedis.setnx("key", "value2");
    System.out.println(n);
    value = jedis.get("key");
    System.out.println(value);
}

执⾏结果

1
value
0
value

psetex

  • 获取到的结果不⼀定刚好 1000. pttl 本⾝也是有时间开销的.
private static void testPsetexAndPttl(Jedis jedis) {
    jedis.psetex("key", 1000, "value");
    long ttl = jedis.pttl("key");
    System.out.println(ttl);
}

执⾏结果

998

incrdecr

private static void testIncrAndDecr(Jedis jedis) {
    jedis.set("key", "0");
    jedis.incr("key");
    System.out.println(jedis.get("key"));

    jedis.decr("key");
    System.out.println(jedis.get("key"));
}

执⾏结果

1
0

incrbydecrby

private static void testIncrByAndDecrBy(Jedis jedis) {
    jedis.set("key", "0");
    jedis.incrBy("key", 10);
    System.out.println(jedis.get("key"));

    jedis.decrBy("key", 5);
    System.out.println(jedis.get("key"));
}

执⾏结果

10
5
列表操作
  1. lpush / lrange
  2. rpush、rpop、lpop
  3. blpop、brpop
  4. llen

lpushlpop

private static void testLpushAndLpop(Jedis jedis) {
    long n = jedis.lpush("key", "1", "2", "3", "4");
    System.out.println(n);

    String value = jedis.lpop("key");
    System.out.println(value);

    value = jedis.lpop("key");
    System.out.println(value);

    value = jedis.lpop("key");
    System.out.println(value);

    value = jedis.lpop("key");
    System.out.println(value);

    value = jedis.lpop("key"); // This may print null if the list is empty.
    System.out.println(value);
}

执⾏结果

4
4
3
2
1
null

rpushrpop

private static void testRpushAndRpop(Jedis jedis) {
    long n = jedis.rpush("key", "1", "2", "3", "4");
    System.out.println(n);

    String value = jedis.rpop("key");
    System.out.println(value);

    value = jedis.rpop("key");
    System.out.println(value);

    value = jedis.rpop("key");
    System.out.println(value);

    value = jedis.rpop("key");
    System.out.println(value);

    value = jedis.rpop("key"); // This may print null if the list is empty.
    System.out.println(value);
}

执⾏结果

4
4
3
2
1
null

lrange

lrange 填写的区间为闭区间.

private static void testLrange(Jedis jedis) {
    jedis.rpush("key", "1", "2", "3", "4");
    List<String> values = jedis.lrange("key", 1, 3);
    System.out.println(values);
}

执⾏结果

[2, 3, 4]

blpop

  • 返回值 List 是个⼆元组. [0] 表⽰ key, [1] 表⽰ value
  • 超时时间设为 0 表⽰死等.
  • 在执⾏同时, 起⼀个 redis-cli, 插⼊数据, 即可看到 blpop 的返回结果.

注意: 在代码中另起⼀个线程, 直接通过当前 jedis 这个连接插⼊数据是不⾏的. 必须另起⼀个 jedis 连接

private static void testBLpop(Jedis jedis) {
    while (true) {
        List<String> values = jedis.blpop(0, "key");
        System.out.println(values);
    }
}

执行结果

# 客⼾端执⾏ rpush key 1 2 3

[key, 1]
[key, 2]
[key, 3]

brpop

  • 使⽤⽅式和 blpop 类似
private static void testBRpop(Jedis jedis) {
    System.out.println("开始调用 brpop");
    while (true) {
        List<String> values = jedis.brpop(0, "key");
        System.out.println(values);
    }
}

执行结果

[key, 3]
[key, 2]
[key, 1]

lindex

private static void testLindex(Jedis jedis) {
    jedis.rpush("key", "1", "2", "3", "4");
    String value = jedis.lindex("key", 2);
    System.out.println(value);
}

执⾏结果

3

linsert

  • 通过 ListPosition.BEFORE 和 ListPosition.AFTER 标识插⼊位置.
private static void testLinsert(Jedis jedis) {
    jedis.rpush("key", "a", "b", "c", "d");
    jedis.linsert("key", ListPosition.BEFORE, "c", "100");
    List<String> values = jedis.lrange("key", 0, -1);
    System.out.println(values);
}

执行结果

 [a, b, 100, c, d]

llen

private static void testLlen(Jedis jedis) {
    jedis.rpush("key", "a", "b", "c", "d");
    long n = jedis.llen("key");
    System.out.println(n);
}

执⾏结果

4
哈希表操作
  1. hset / hget
  2. hexists
  3. hdel
  4. hkeys / hvals
  5. hmget / hmset

hsethget

private static void testHsetAndHget(Jedis jedis) {
    jedis.hset("key", "name", "zhangsan");
    jedis.hset("key", "age", "20");
    String name = jedis.hget("key", "name");
    System.out.println(name);
    String age = jedis.hget("key", "age");
    System.out.println(age);
}

执⾏结果

zhangsan
20

hexistshdel

private static void testHexistsAndHdel(Jedis jedis) {
    jedis.hset("key", "name", "zhangsan");
    boolean ok = jedis.hexists("key", "name");
    System.out.println(ok);

    jedis.hdel("key", "name");
    ok = jedis.hexists("key", "name");
    System.out.println(ok);
}

执⾏结果

true
false

hkeyshvalues

private static void testHkeysAndHvalues(Jedis jedis) {
    jedis.hset("key", "name", "zhangsan");
    jedis.hset("key", "age", "20");

    Set<String> keys = jedis.hkeys("key");
    System.out.println(keys);

    List<String> values = jedis.hvals("key");
    System.out.println(values);
}

执⾏结果

[name, age]
[zhangsan, 20]

hmget

private static void testHmget(Jedis jedis) {
    jedis.hset("key", "name", "zhangsan");
    jedis.hset("key", "age", "20");
    List<String> values = jedis.hmget("key", "name", "age");
    System.out.println(values);
}

执⾏结果

[zhangsan, 20]

hlen

private static void testHlen(Jedis jedis) {
    jedis.hset("key", "name", "zhangsan");
    jedis.hset("key", "age", "20");
    long n = jedis.hlen("key");
    System.out.println(n);
}

执⾏结果

2

hincrbyhincrbyfloat

private static void testHIncrByAndIncrByFloat(Jedis jedis) {
    jedis.hset("key", "age", "20");
    long n = jedis.hincrBy("key", "age", 10);
    System.out.println(n);
    String value = jedis.hget("key", "age");
    System.out.println(value);

    double dn = jedis.hincrByFloat("key", "age", 0.5);
    System.out.println(dn);
    value = jedis.hget("key", "age");
    System.out.println(value);
}

执⾏结果

30
30
30.5
30.5
集合操作
  1. sadd / smembers
  2. sismember
  3. scard
  4. spop
  5. sinter / sinterstore

saddsmembers

private static void testSaddAndSmembers(Jedis jedis) {
    jedis.sadd("key", "aaa", "bbb", "ccc");
    Set<String> members = jedis.smembers("key");
    System.out.println(members);
}

执⾏结果

[aaa, ccc, bbb]

sremsismember

private static void testSremAndSismember(Jedis jedis) {
    jedis.sadd("key", "aaa", "bbb", "ccc");
    boolean ok = jedis.sismember("key", "aaa");
    System.out.println(ok);
    long n = jedis.srem("key", "aaa", "bbb");
    System.out.println(n);

    ok = jedis.sismember("key", "aaa");
    System.out.println(ok);
}

执⾏结果

true
2
false

scard

private static void testScard(Jedis jedis) {
    jedis.sadd("key", "aaa", "bbb", "ccc");
    long n = jedis.scard("key");
    System.out.println(n);
}

执⾏结果

3

sinter

private static void testSinter(Jedis jedis) {
    jedis.sadd("key1", "aaa", "bbb", "ccc");
    jedis.sadd("key2", "aaa", "bbb", "ddd");

    Set<String> results = jedis.sinter("key1", "key2");
    System.out.println(results);
}

执行结果

[aaa, bbb]

sunion

private static void testSunion(Jedis jedis) {
    jedis.sadd("key1", "aaa", "bbb", "ccc");
    jedis.sadd("key2", "aaa", "bbb", "ddd");

    Set<String> results = jedis.sunion("key1", "key2");
    System.out.println(results);
}

执⾏结果

 [aaa, ccc, bbb, ddd]

sdiff

private static void testSdiff(Jedis jedis) {
    jedis.sadd("key1", "aaa", "bbb", "ccc");
    jedis.sadd("key2", "aaa", "bbb", "ddd");

    Set<String> results = jedis.sdiff("key1", "key2");
    System.out.println(results);
}

执⾏结果

[ccc]
有序集合操作
  1. zadd / zrange
  2. zcard
  3. zrem
  4. zscore
  5. zrank

zaddzrange

  • zrange 通过下标获取元素. 闭区间
private static void testZaddAndZrange(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典韦");
    jedis.zadd("key", 92, "关羽");
    jedis.zadd("key", 70, "刘备");

    List<String> members = jedis.zrange("key", 0, 4);
    System.out.println(members);

    List<Tuple> membersWithScore = jedis.zrangeWithScores("key", 0, 4);
    System.out.println(membersWithScore);
}

执⾏结果

[刘备, 关⽻, 典⻙, 赵云, 吕布]
[[刘备,70.0], [关⽻,92.0], [典⻙,95.0], [赵云,98.0], [吕布,100.0]]

zremzcard

private static void testZremAndZcard(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典韦");
    jedis.zadd("key", 92, "关羽");
    jedis.zadd("key", 70, "刘备");

    long n = jedis.zcard("key");
    System.out.println(n);

    n = jedis.zrem("key", "吕布", "赵云");
    System.out.println(n);
    n = jedis.zcard("key");
    System.out.println(n);
}

执⾏结果

5
2
3

zcount

  • 获取指定分数区间中的元素个数. 闭区间.
private static void testZcount(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典韦");
    jedis.zadd("key", 92, "关羽");
    jedis.zadd("key", 70, "刘备");

    long n = jedis.zcount("key", 92, 98);
    System.out.println(n);
}

执⾏结果

3

zpopmaxzpopmin

private static void testZpopmaxAndZpopmin(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典⻙");
    jedis.zadd("key", 92, "关⽻");
    jedis.zadd("key", 70, "刘备");

    Tuple tuple = jedis.zpopmax("key");
    System.out.println(tuple);

    tuple = jedis.zpopmin("key");
    System.out.println(tuple);
}

执⾏结果

[吕布,100.0]
[刘备,70.0]

zrank

  • 获取指定 member 的下标
private static void testZrank(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典⻙");
    jedis.zadd("key", 92, "关⽻");
    jedis.zadd("key", 70, "刘备");

    long n = jedis.zrank("key", "赵云");
    System.out.println(n);

    n = jedis.zrevrank("key", "赵云");
    System.out.println(n);
}

执⾏结果

3
1

zscore

private static void testZscore(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");
    jedis.zadd("key", 98, "赵云");
    jedis.zadd("key", 95, "典⻙");
    jedis.zadd("key", 92, "关⽻");
    jedis.zadd("key", 70, "刘备");

    double score = jedis.zscore("key", "赵云");
    System.out.println(score);
}

执⾏结果

98.0

zincrby

private static void testZincrby(Jedis jedis) {
    jedis.zadd("key", 100, "吕布");

    double n = jedis.zincrby("key", 10, "吕布");
    System.out.println(n);

    n = jedis.zincrby("key", -20, "吕布");
    System.out.println(n);
}

执⾏结果

110.0
90.0

zinterstore

private static void testZinterstore(Jedis jedis) {
    jedis.zadd("key1", 100, "吕布");
    jedis.zadd("key1", 98, "赵云");
    jedis.zadd("key1", 95, "典⻙");

    jedis.zadd("key2", 100, "吕布");
    jedis.zadd("key2", 98, "赵云");
    jedis.zadd("key2", 92, "关⽻");

    long n = jedis.zinterstore("key3", "key1", "key2");
    System.out.println(n);
    List<Tuple> tuples = jedis.zrangeWithScores("key3", 0, -1);
    System.out.println(tuples);
}

执⾏结果

2
[[赵云,196.0], [吕布,200.0]]

zunionstore

private static void testZunionstore(Jedis jedis) {
    jedis.zadd("key1", 100, "吕布");
    jedis.zadd("key1", 98, "赵云");
    jedis.zadd("key1", 95, "典⻙");

    jedis.zadd("key2", 100, "吕布");
    jedis.zadd("key2", 98, "赵云");
    jedis.zadd("key2", 92, "关⽻");

    long n = jedis.zunionstore("key3", "key1", "key2");
    System.out.println(n);
    List<Tuple> tuples = jedis.zrangeWithScores("key3", 0, -1);
    System.out.println(tuples);
}

执⾏结果

4
[[关⽻,92.0], [典⻙,95.0], [赵云,196.0], [吕布,200.0]]

Jedis 这个库本质就是针对 Redis 的各种命令进行了封装,调用某个方法,就相当于 Redis 客户端中敲下了对应的命令,详细可以去 GitHub 看看 Jedis官方网站,其中有个 Jedis文档

访问集群

使⽤ JedisCluster 类代替 Jedis 类即可.

需要在创建实例的时候, 把多个节点的地址, 都设置进去.

JedisCluster 提供的⽅法和 Jedis 基本⼀致. 都和 Redis 命令是对应的. 具体细节我们不再演⽰了

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("172.30.0.101", 6379));
        nodes.add(new HostAndPort("172.30.0.102", 6379));
        nodes.add(new HostAndPort("172.30.0.103", 6379));
        nodes.add(new HostAndPort("172.30.0.104", 6379));
        nodes.add(new HostAndPort("172.30.0.105", 6379));
        nodes.add(new HostAndPort("172.30.0.106", 6379));
        nodes.add(new HostAndPort("172.30.0.107", 6379));
        nodes.add(new HostAndPort("172.30.0.108", 6379));
        nodes.add(new HostAndPort("172.30.0.109", 6379));

        try (JedisCluster jedisCluster = new JedisCluster(nodes)) {
            jedisCluster.set("k1", "111");
            String value = jedisCluster.get("k1");
            System.out.println(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意! 由于此处我们的代码是需要访问整个 redis 集群, 因此不能直接在 windows 上运⾏程序了. (上述docker 容器的 ip 都是 windows 主机上⽆法直接访问的).

需要把整个程序打成 jar 包, 上传到 Linux 中, 并通过下列⽅式运⾏.

java -jar [jar包名]

Redis Java 集成到 Spring Boot

使⽤ Spring Boot 连接 Redis 单机
创建项⽬

勾选 NoSQL 中的 Spring Data Redis

当然, 把 Web 中的 Spring Web 也勾选⼀下. ⽅便写接⼝进⾏后续测试.

配置 redis 服务地址

在 application.yml 中配置.

spring:
  redis:
    host: 127.0.0.1
    port: 9999
创建 Controller

创建 MyController

由于当前只是写简单的测试代码, 我们就不进⾏分层了. 就只创建个简单的 Controller 即可.

后续 Redis 测试的各种方法,都通过这个 Controller 提供的 http 接口来触发,访问对应的接口即可在网页和控制台看见对应的结果、输出了

@RestController
public class MyController {
    @Autowired
    private StringRedisTemplate redisTemplate; 
}

此处需要引⼊ StringRedisTemplate 实例.

前面的 jedis,都是通过 Jedis 对象里的各种方法来操作 Redis,现在 Spring 是通过 StringRedisTemplate 来操作 Redis,这是 RedisTemplate 的子类,专门用来处理文本数据的,这个类提供的方法和 Jedis 提供的方法还是有很大差异的

此处的 RedisTemplate 是把这些操作 Redis 的方法分成了几个类别,这是希望通过上述重新的封装,让接口用起来更简单

关于 删库 和 ping 还有其他一些操作就没有直接提供了,但可以通过 execute 方法,这个方法里的参数是 RedisCallBack<T> ,函数式接口,即回调函数,写我们要执行的 redis 命令,这个回调就会被 RedisTemplate 内部进行执行了,在方法内部这么写即可删库,或者其他你想的操作

redisTemplate.execute((RedisConnection connection) -> {
    // execute 要求回调方法中必须写 return 语句,返回个东西
    // 这个回调返回的对象,就会作为 execute 本身的返回值
    connection.flushAll();
    return null;
});
使⽤ String

.opsForValue 是专门操作字符串的

@GetMapping("/testString")
@ResponseBody
public String testString() {
    redisTemplate.opsForValue().set("key", "value");
    String value = redisTemplate.opsForValue().get("key");
    System.out.println(value);

    redisTemplate.delete("key");
    return "OK";
}
使⽤ List
@GetMapping("/testList")
@ResponseBody
public String testList() {
    redisTemplate.opsForList().leftPush("key", "a");
    redisTemplate.opsForList().leftPushAll("key", "b", "c", "d");
    List<String> values = redisTemplate.opsForList().range("key", 1, 2);
    System.out.println(values);

    redisTemplate.delete("key");
    return "OK";
}
使⽤ Hash
@GetMapping("/testHashmap")
@ResponseBody
public String testHashmap() {
    redisTemplate.opsForHash().put("key", "name", "zhangsan");
    String value = (String) redisTemplate.opsForHash().get("key", "name");
    System.out.println(value);

    redisTemplate.opsForHash().delete("key", "name");
    boolean ok = redisTemplate.opsForHash().hasKey("key", "name");
    System.out.println(ok);

    redisTemplate.delete("key");
    return "OK";
}

使⽤ Set
@GetMapping("/testSet")
@ResponseBody
public String testSet() {
    redisTemplate.opsForSet().add("key", "aaa", "bbb", "ccc");
    boolean ok = redisTemplate.opsForSet().isMember("key", "aaa");
    System.out.println(ok);

    redisTemplate.opsForSet().remove("key", "aaa");
    long n = redisTemplate.opsForSet().size("key");
    System.out.println(n);

    redisTemplate.delete("key");
    return "OK";
}
使⽤ ZSet
@GetMapping("/testZSet")
@ResponseBody
public String testZSet() {
    redisTemplate.opsForZSet().add("key", "吕布", 100);
    redisTemplate.opsForZSet().add("key", "赵云", 98);
    redisTemplate.opsForZSet().add("key", "典⻙", 95);

    Set<String> values = redisTemplate.opsForZSet().range("key", 0, 2);
    System.out.println(values);

    long n = redisTemplate.opsForZSet().count("key", 95, 100);
    System.out.println(n);

    redisTemplate.delete("key");
    return "OK";
}
执⾏结果

运⾏程序, 构造上述请求, 即可在服务器的控制台中看到执⾏结果.

使⽤ Spring Boot 连接 Redis 集群
配置 redis 集群地址
spring:
  redis:
    cluster:
      nodes:
        - 172.30.0.101:6379
        - 172.30.0.102:6379
        - 172.30.0.103:6379
        - 172.30.0.104:6379
        - 172.30.0.105:6379
        - 172.30.0.106:6379
        - 172.30.0.107:6379
        - 172.30.0.108:6379
        - 172.30.0.109:6379
    lettuce:
      cluster:
        refresh:
          adaptive: true
          period: 2000

下⽅的 lettuce 系列配置, ⽬的是为了⾃动刷新集群的拓扑结构. 当集群中有节点宕机/加⼊新节点之后, 我们的代码能够⾃动感知到集群的变化.

改完配置之后, 其他代码⽆需做出任何调整, 直接就可以正常运⾏了.

注意!

由于上述 ip 都是 docker 容器的 ip, 在 windows 主机上不能直接访问.

因此需要把程序打成 jar 包, 部署到 linux 上, 再通过 java -jar [jar包名] 的⽅式执⾏.

Spring Data Redis 官方文档
Spring Data Redis 官方中文文档

小结

Spring Boot 2 系列内置的 Redis 是 Lettuce, 和 Jedis 的使⽤还是存在⼀定的差异.

对于 Jedis 来说, 各个⽅法和 Redis 的命令基本是⼀致的.

⽽集成到 Spring Boot 之后, 接⼝上和原始 Redis 命令存在部分差别, 但是使⽤起来也并不困难, 只要⼤家熟悉 Redis 的基本操作, 还是很容易可以通过⽅法名字理解⽤法的.

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值