下面介绍一下在Java程序中如何使用Redis客户端

Jedis

Maven依赖

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.1</version>
</dependency>

简单使用

字符串

1
2
3
4
5
6
>//输出结果: OK
jedis.set("hello", "world");
//输出结果: world
jedis.get("hello");
//输出结果:1
jedis.incr("counter");

哈希

1
2
3
4
>jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
//输出结果 : {f1=v1, f2=v2}
jedis.hgetAll("myhash");

列表

1
2
3
4
5
>jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
//输出结果 : [1, 2, 3]
jedis.lrange("mylist", 0, -1);

集合

1
2
3
4
5
>jedis.sadd(" myset", "a");
jedis.sadd(" myset", "b");
jedis.sadd(" myset", "a");
//输出结果 : [b, a]
jedis.smembers("myset");

有序集合

1
2
3
4
5
>jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
//输出结果 : [[["james"],33.0], [["peter"],66.0], [["tom"],99.0]]
jedis.zrangeWithScores("myzset", 0, -1);

Jedis连接池

jedis直连

每次操作创建一个jedis对象,执行完毕后关闭连接,对应的就是一次Tcp连接。

1
2
3
4
5
6
//生成一个jedis对象,这个对象负责和指定Redis节点进行通信
Jedis jedis = new Jedis("119.23.226.29", 6379);
//带密码需要执行认证方法
//jedis.auth("123456");
jedis.set("hello", "world");
String value = jedis.get("hello");

jedis连接池

预先生成一批jedis连接对象放入连接池中,当需要对redis进行操作时从连接池中借用jedis对象,操作完成后归还。这样jedis对象可以重复使用,避免了频繁创建socket连接,节省了连接开销。

1
2
3
4
>JedisPool pool = new JedisPool("127.0.0.1", 6379);
Jedis jedis = pool.getResource();
jedis.set("hello", "world");
String value = jedis.get("hello");

lettuce

Lettuce 是一个可伸缩线程安全的 Redis 客户端。多个线程可以共享同一个 RedisConnection。它利用优秀 netty NIO 框架来高效地管理多个连接。

RedisURI是redis连接的一些标准信息,比如需要提供数据库名称,密码,url,超时时间等。有三种方式可以创建:

1
2
3
RedisURI.create("redis://localhost/");
RedisURI.Builder.redis("localhost", 6379).auth("password").database(1).build();
new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);
1
2
3
4
5
6
事务命令
multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视key
unwatch:清除监视key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RedisURI redisUri = RedisURI.builder()// <1> 创建单机连接的连接信息
.withHost("127.0.0.1")
.withPort(6379)
.withTimeout(Duration.of(10, ChronoUnit.SECONDS))
.build();
RedisClient redisClient = RedisClient.create(redisUri);// <2> 创建客户端
GenericObjectPool<StatefulRedisConnection<String, String>> genericObjectPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), new GenericObjectPoolConfig<>());
StatefulRedisConnection<String, String> connection = genericObjectPool.borrowObject();
//StatefulRedisConnection<String, String> connection = redisClient.connect();// <3> 创建线程安全的连接
RedisCommands<String, String> redisCommands = connection.sync();// <4> 创建同步命令
//redisCommands.multi();
//redisCommands.set("key", "value");
//redisCommands.set("key2", "value2");
//redisCommands.exec();
SetArgs setArgs = SetArgs.Builder.nx().ex(5);
String result = redisCommands.set("name", "chen", setArgs);
System.out.println(result);
result = redisCommands.get("name");
System.out.println(result);
// ... 其他操作
connection.close();// <5> 关闭连接
redisClient.shutdown();// <6> 关闭客户端

Redisson

Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发

1
2
3
4
5
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
redissonClient.getBucket("name").set("chen");
System.out.println(redissonClient.getBucket("name").get());

之后有机会好好的补充一下

lua脚本

Redis中为什么引入Lua脚本?

Redis是高性能的key-value内存数据库,它帮助我们解决了大部分业务问题;提供丰富的指令集合,据官网上统计有200多个命令。这些命令显然已经满足了我们的常规的业务场景需求。但是在某些特殊的场景下,业务需要原子性操作,redis原有的命令是无法完成,所以需要额外开发实现原子操作。

因为这样的问题,Redis为开发者提供了lua脚本的支持,用户可以向服务器发送lua脚本来执行自定义动作,以此获取脚本的响应数据。Redis本身又是单线程执行lua脚本,保证了lua脚本在处理逻辑过程中不会被任意其它请求打断

什么是Lua

Lua是一种轻量小巧脚本语言,用标准C语言编写并以源代码形式开放。

其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。

比如:Lua脚本用在很多游戏上,主要是Lua脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。

使用Lua脚本的好处:

1
2
3
4
5
① 支持原子性操作 - Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务

② 降低网络开销 - 将多个请求通过脚本的形式一次发送到服务器,减少了网络的时延

③ 脚本复用 - 客户端发送的脚本可支持永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

Redis中Lua的常用命令

命令不多,就下面这几个:
- EVAL
- EVALSHA
- SCRIPT LOAD - SCRIPT EXISTS
- SCRIPT FLUSH
- SCRIPT KILL

EVAL命令

命令格式:EVAL script numkeys key [key …] arg [arg …]
- script参数是一段 Lua5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数
- numkeys指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0
- key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
- arg [arg …] 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"

// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"

// 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second
// 其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表,
// 相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
// 举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"


// 例4:使用了redis为lua内置的redis.call函数
// 脚本内容为:先执行SET命令,在执行EXPIRE命令
// numkeys=1,keys数组有一个元素userAge(代表redis的key)
// arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44

通过上面的例4,我们可以发现,脚本中使用redis.call()去调用redis的命令。
在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: redis.call() 和 redis.pcall()
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误,差别如下:

错误处理
当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

1
2
3
4
5
127.0.0.1:6379> lpush foo a
(integer) 1

127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

和 redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

1
2
127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

SCRIPT LOAD命令 和 EVALSHA命令

SCRIPT LOAD命令格式:SCRIPT LOAD script
EVALSHA命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]

这两个命令放在一起讲的原因是:EVALSHA 命令中的sha1参数,就是SCRIPT LOAD 命令执行的结果。

SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

1
2
3
4
5
6
7
8
9
10
11
## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"

## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 43

SCRIPT EXISTS 命令

命令格式:SCRIPT EXISTS sha1 [sha1 …]
作用:给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

1
2
3
4
5
6
7
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe346
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345 6aeea4b3e96171ef835a78178fceadf1a5dbe366
1) (integer) 1
2) (integer) 0

SCRIPT FLUSH 命令

命令格式:SCRIPT FLUSH
作用:清除Redis服务端所有 Lua 脚本缓存

1
2
3
4
5
6
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0

SCRIPT KILL 命令

命令格式:SCRIPT FLUSH
作用:杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

Redis执行Lua脚本文件

在第二章中介绍的命令,是在redis客户端中使用命令进行操作。该章节介绍的是直接执行 Lua 的脚本文件。

编写Lua脚本文件

1
2
3
4
5
6
7
8
9
10
local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end

执行Lua脚本文件

1
2
执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …] 
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi

此处敲黑板,注意啦!!!
“–eval”而不是命令模式中的”eval”,一定要有前端的两个-
脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值
key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错

1
2
3
4
5
6
7
8
9
10
11
12
13
## Redis客户端执行
127.0.0.1:6379> set userName zhangsan
OK
127.0.0.1:6379> get userName
"zhangsan"

## linux服务器执行
## 第一次执行:compareAndSet成功,返回1
## 第二次执行:compareAndSet失败,返回0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 0

使用Lua控制IP访问频率

需求:实现一个访问频率控制,某个IP在短时间内频繁访问页面,需要记录并检测出来,就可以通过Lua脚本高效的实现。
小声说明:本实例针对固定窗口的访问频率,而动态的非滑动窗口。即:如果规定一分钟内访问10次,记为超限。在本实例中前一分钟的最后一秒访问9次,下一分钟的第1秒又访问9次,不计为超限。
脚本如下:

1
2
3
4
5
6
7
8
9
10
11
local visitNum = redis.call('incr', KEYS[1])

if visitNum == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end

if visitNum > tonumber(ARGV[2]) then
return 0
end

return 1;

演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
## LimitIP:127.0.0.1为key, 10 3表示:同一IP在10秒内最多访问三次
## 前三次返回1,代表未被限制;第四、五次返回0,代表127.0.0.1这个ip已被拦截
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 0