Redis学习笔记
进阶篇
Redis消息模式
队列模式
-
原理:使用list类型的 LPUSH 和 RPOP 实现消息队列:
-
注意事项:
- 消息接收方如果不知道队列中是否有消息,会一直发送 RPOP 命令,如果这样的话,会每一次都建立一次链接,显然不好
- 可以使用 BRPOP 命令,它如果从队列中取不出类数据,会一直阻塞,在一定返回内没有取出则返回null
-
缺点:
- 做消费者确认ACK麻烦,不能保证消费者消费消息后是否成功处理的问题(宕机或处理异常等),通常需要维护一个Pending列表,保证消息处理确认。
- 不能做广播模式,如pub/sub,消息发布/订阅模型。
- 不能重复消费,一旦消费就会被删除。
- 不支持分组消费
发布订阅模式
- 订阅消息(subscribe)
subscribe yw-channel
- 发布消息(publish)
publish yw-channel "hello,everyone!!!"
- Redis发布订阅命令
- 优点:
- 典型的广播模式,一个消息可以发布到多个消费者;
- 多信道订阅,消费者可以同时订阅多个信道,从而接收多类消息;
- 消息即时发送,消息不用等待消费者读取,消费者会自动接收到信道发布的消息。
- 缺点:
- 消息一旦发布,不能接收。换句话就是发布时若客户端不在线,则消息丢失,不能寻回。
- 不能保证每个消费者接收的时间是一致的。
- 若消费者客户端出现消息积压,到一定程度,会被强制断开,导致消息意外丢失。通常发生在消息的生产远大于消费速度时。
- 可见,Pub/Sub 模式不适合做消息存储,消息积压类的业务,而是擅长处理广播,即时通讯,即时反馈的业务。
基于Sorted-Set的实现
- Sortes Set(有序列表),类似于java的SortedSet和HashMap的结合体,一方面它是一个set,保证内部value的唯一性,另一方面它可以给每个value赋予一个score,代表这个value的排序权重。内部实现是“跳跃表”。
- 有序集合的方案是在自己确定消息顺ID时比较常用,使用集合成员的Score来作为消息ID,保证顺序,还可以保证消息ID的单调递增。通常可以使用时间戳+序号的方案。确保了消息ID的单调递增,利用SortedSet的依据Score排序的特征,就可以制作一个有序的消息队列了。
- 优点:就是可以自定义消息ID,在消息ID有意义时,比较重要。
- 缺点:缺点也明显,不允许重复消息(因为是集合),同时消息ID确定有错误会导致消息的顺序出错。
Redis Stream
- Redis 5.0 全新的数据类型:Streams。
- 官方把它定义为:以更抽象的方式建模日志的数据结构。Redis的streams主要是一个append only(AOF)的数据结构,至少在概念上它是一种在内存中表示的抽象数据类型,只不过它们实现了更强大的操作,以克服日志文件本身的限制。
- 如果你了解MQ,那么可以把streams当做基于内存的MQ。如果你还了解kafka,那么甚至可以把streams当做基于内存的kafka。listpack存储信息,Rax组织listpack 消息链表listpack是对ziplist的改进,它比ziplist少了一个定位最后一个元素的属性。
- 另外,这个功能有点类似于redis以前的Pub/Sub,但是也有基本的不同:
- streams支持多个客户端(消费者)等待数据(Linux环境开多个窗口执行XREAD即可模拟),并且每个客户端得到的是完全相同的数据。
- Pub/Sub是发送忘记的方式,并且不存储任何数据;而streams模式下,所有消息被无期限追加在streams中,除非用于显示执行删除(XDEL)。XDEL 只做一个标记位,其实信息和长度还在。
- streams的Consumer Groups也是Pub/Sub无法实现的控制方式。
streams数据结构
- 它主要由消息、生产者、消费者、消费组四部分组成。
- streams数据结构本身非常简单,但是streams依然是Redis到目前为止最复杂的类型,其原因是实现的一些额外的功能:一系列的阻塞操作允许消费者等待生产者加入到streams的新数据。另外还有一个称为Consumer Groups的概念,Consumer Group概念最先由kafka提出,Redis有一个类似实现,和kafka的Consumer Groups的目的是一样的:允许一组客户端协调消费相同的信息流!
演示
- 发布消息:
127.0.0.1:6379> xadd mystream * message apple
"1598541692493-0"
127.0.0.1:6379> xadd mystream * message orange
"1598541701959-0"
127.0.0.1:6379> keys *
1) "mystream"
127.0.0.1:6379>
- 读取消息:可以打开多个客户端读取消息
127.0.0.1:6379> xrange mystream - +
1) 1) "1598541692493-0"
2) 1) "message"
2) "apple"
2) 1) "1598541701959-0"
2) 1) "message"
2) "orange"
127.0.0.1:6379>
- 阻塞读取:
127.0.0.1:6379> xread block 0 streams mystream $
- 发布新消息:
127.0.0.1:6379> xadd mystream * message blueberry
- 创建消费组:
127.0.0.1:6379> xgroup create mystream mygroup1 0
OK
127.0.0.1:6379> xgroup create mystream mygroup2 0
OK
127.0.0.1:6379>
- 通过消费组读取消息:
127.0.0.1:6379> xreadgroup group mygroup1 aaa count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1598542393475-0"
2) 1) "message"
2) "apple"
2) 1) "1598542397495-0"
2) 1) "message"
2) "orange"
127.0.0.1:6379> xreadgroup group mygroup1 bbb count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1598542404932-0"
2) 1) "message"
2) "blueberry"
127.0.0.1:6379> xreadgroup group mygroup1 ccc count 2 streams mystream >
(nil)
127.0.0.1:6379> xreadgroup group mygroup2 ccc count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1598541692493-0"
2) 1) "message"
2) "apple"
2) 1) "1598541701959-0"
2) 1) "message"
2) "orange"
127.0.0.1:6379>
适用场景
- 可用作实时通信等,大数据分析,异地数据备份等
Redis事务
- 严格意义上说 redis事务只是个批处理 有隔离性 但是没有原子性
Redis事务介绍
- Redis的事务是通过MULTI、EXEC、DISCARD和WATCH这四个命令来完成的
- Redis的单个命令都是原子的,所以这里需要确保事务性的对象是命令集合
- Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断执行
- Redis 不支持回滚操作
事务命令
MULTI
- 语法:MULTI
- 说明:用于标记事务块的开始,Redis会将后续的命令逐个放入队列,然后使用EXEC命令原子化地执行这个命令序列
EXEC
- 语法:EXEC
- 说明:在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态
DISCARD
- 语法:DISCARD
- 说明:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态
WATCH
- 语法:WATCH key [key …]
- 说明:当某个【事务需要按条件执行】时,就要使用这个命令将给定的【键设置为受监控】的状态
- 注意:使用该命令可以实现 Redis 乐观锁
UNWATCH
- 语法:UNWATCH
- 说明:清除所有先前为一个事务监控的键
事务演示
事务失败处理
- Redis语法错误(编译期)
- Redis运行错误
Redis不支持事务回滚(为什么?)
- 大多数事务失败是因为语法错误或者类型错误,这两种错误在开发阶段都是可以预见的
- Redis为了性能方面就忽略了事务回滚
Redis和lua整合
什么是lua?
lua 是一种轻量级的脚本语言,用标准C语言编写并以源码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis中使用lua的好处?
- 减少网络开销,在lua脚本中可以把多个命令放在同一个脚本中运行
- 原子操作,redis会将整个脚本作为一个整体脚本,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
- 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑
lua的安装(了解)
- 下载地址:http://www.lua.org/download.html
可以本地下载上传到linux,也可以使用curl命令在linux系统中进行在线下载
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
- 安装
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz
cd lua-5.3.5/
make linux
make install
如果报错,找不到 readline/readline.h,可以通过yum命令安装
yum -y install readline-devel ncurses-devel
安装完以后再执行:
make linux / make install
最后,直接输入 lua 命令即可进入 lua 的控制台:
lua常见语法(了解)
详见:http://www.runoob.com/lua/lua-tutorial.html
Redis整合lua脚本
从Redis2.6.0版本开始,通过内置的 lua 编译/解释器,可以使用 EVAL 命令对 lua 脚本进行求值
EVAL命令
- 通过执行 redis 的 eval 命令,可以运行一段 lua 脚本
- 在 redis 客户端中,执行以下命令
EVAL script numkeys key [key ...] arg [arg ...]
- 命令说明:
* 【script参数】:是一段 Lua 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数
* 【numkeys参数】:用于指定键名参数的个数
* 【key [key...]参数】:从 EVAL 的第三个参数开始算起,使用了[numkeys]个键(key),表示在脚本中所用到的那些Redis键(key),这些键名参数可以再 Lua 中通过全局变量 KEYS 数组,用1为基址的形式访问(KEYS[1],KEYS[2],...)
* 【arg [arg ...]参数】:可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似(ARGV[1],ARGV[2],...)
- 演示:
lua脚本中调用 Redis 命令
- redis.call():返回值就是 redis 命令执行的返回值;如果出错,则返回错误信息,不继续执行
- redis.pcall():返回值就是 redis 命令执行的返回值;如果出错,则记录错误信息,继续执行
- 注意事项:在脚本中,使用 return 语句将返回值返回给客户端,如果没有 return,则返回nil
- 演示:
EVALSHA
* EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)
* Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传输脚本主体并不是最佳选择
* 为了减少带宽的消耗,Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和 (sum)
EVALSHA命令的表现如下:
- 如果服务器还记得给定的 SHA1 校验和所指定的脚本,那么执行这个脚本
- 如果服务器不记得给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用EVAL代替EVALSHA
SCRIPT命令
- SCRIPT FLUSH:清除所有脚本缓存
- SCRIPT EXISTS:根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
- SCRIPT LOAD:将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
- SCRIPT KILL:杀死当前正在运行的脚本
- 演示
redis-cli --eval
- 可以使用 redis-cli --eval 命令指定一个 lua 脚本文件去运行
- 脚本文件(redis.lua),内容如下:
return redis.call('set', KEYS[1], ARGV[1]);
- 在 redis 客户机,执行脚本命令:
redis-cli --eval redis.lua test , '测试'
- 命令格式说明
* --eval:告诉redis客户端去执行后面的lua脚本
* redis.lua:具体的lua脚本文件名称
* test:lua脚本中需要的key
* '测试':lua脚本中需要的value
- 注意事项:上面命令中的 keys 和 values 中间需要使用逗号隔开,并且逗号两边都要有空格
lua和redis整合实战
- 通过 lua 脚本获取指定的 key 的 List 中的所有数据:test1.lua
local key = KEYS[1];
local list = redis.call('lrange', key, 0, -1);
return list;
这里面的 redis.call 就是用来执行 redis 中 list 的 lrange 命令,接下来我通过 lpush 给 person 塞入三条数据,如下:
[root@centos-host bin]# ./redis-cli
127.0.0.1:6379> lpush person mary jack peter
(integer) 3
127.0.0.1:6379>
[root@centos-host bin]# ./redis-cli --eval test1.lua person
1) "peter"
2) "jack"
3) "mary"
- 根据外面传过来的 IDList 做集合去重的 lua 脚本逻辑
local key = KEYS[1];
local args = ARGV[1];
local result = {};
for m,n in ipairs(args) do
local ishit = redis.call('sismember', key, n);
if(ishit) then
table.insert(result, 1, n);
end
end
return result;
- 找到hash中age小于指定值的所有数据
local result = {};
local myperson = KEYS[1];
local nums = ARGV[1];
local myresult = redis.call('hkeys', myperson);
for i,v in ipairs(myresult) do
local hval = redis.call('hget', myperson, v);
redis.log(redis.LOG_WARNING, hval);
if(tonumber(hval) < tonumber(nums)) then
table.insert(result, 1, v);
end
end
return result;
Redis实现分布式锁
锁的处理
- 单应用中使用锁(单进程多线程):synchronized、ReentrantLock
- 分布式应用中使用锁(多进程多线程):分布式锁是控制分布式系统之间同步访问共享资源的一种方式
分布式锁的实现方式
- 基于数据库的乐观锁实现分布式锁
- 基于 Zookeeper 临时节点的分布式锁
- 基于 Redis 的分布式锁
分布式锁的注意事项
- 互斥性:在任意时刻,只有一个客户端能持有锁
- 同一性:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
- 可重入性:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端加锁
实现分布式锁
获取锁
在 SET 命令中,有很多选项可用来修改命令行为:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX:设置指定的到期时间(单位:秒)
- PX:设置指定的到期时间(单位:毫秒)
- NX:仅在键不存在时设置键
- XX:只有在已存在时才设置
方式1(使用set命令实现)–推荐
/**
* 使用redis的set命令实现获取分布式锁
* @Param lockKey 就是锁
* @Param requestId 请求ID,保证同一性
* @Param expireTime 过期时间,避免死锁
* @return
*/
public static boolean getLock(String lockKey, String requestId, int expireTime){
// NX:保证互斥性
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
if("OK".equals(result)){
return true;
}
return false;
}
方式2(使用setnx命令实现)
public static boolean getLock(String lockKey, String requestId, int expireTime){
Long result = jedis.setnx(lockKey, requestId);
if(result == 1){
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
释放锁
方式1(del命令实现)
/**
* 释放锁
*/
public static void releaseLock(String lockKey, String requestId){
if(requestId.equals(jedis.get(lockKey))){
jedis.del(lockKey);
}
}
方式2(redis+lua脚本实现)–推荐
public static void releaseLock(String lockKey, String requestId){
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if(result.equals(1L)){
return true;
}
return false;
}