上一篇我们大致了解了redis的基本数据类型和数据结构,所以大致对于我们存储的数据在内存中有了一个比较模糊的样子了。并且对一些命令也了解了一些,因为针对不同的应用场景,这么命令可以起到不同的作用,就算平时我们不太会用,说简单点,万一面试官问到呢,所以呀,知道总比不知道好。然后呢,我们还要了解下redis的一些高级特性的,比如持久化、内存淘汰机制和过期机制等。好了,废话不要太多,直入主题。
发布订阅模式
我们知道list可以实现队列或者栈,但是用这个来实现队列的话是有局限性的,就是我们得再项目中不同的监听这个list队列的。这肯定说比较消耗内存的,但是如果设置sleep一会再去list队列中拿数据,这又会造成消息消费不及时问题,或者在产生大量消息时没有及时处理redis会消耗内存。并且不支持一对多,反正还是会有很多缺点滴。
所以就有发布订阅这种模式,发布者和订阅者没有直接关系,实现了解藕。这种模式说首先会有很多频道channel,然后生产者向channel发送消息,订阅者可以订阅多个channel接收消息。
订阅消息支持符号:
支持?和*占位符。?代表一个字符,*代表 0 个或者多个字符。
操作命令
1:PSUBSCRIBE pattern [pattern ...]
订阅一个或多个符合给定模式的频道。
2:PUBSUB subcommand [argument [argument ...]]
查看订阅与发布系统状态。
3:PUBLISH channel message
将信息发送到指定的频道。
4:PUNSUBSCRIBE [pattern [pattern ...]]
退订所有给定模式的频道。
5:SUBSCRIBE channel [channel ...]
订阅给定的一个或多个频道的信息。
6:UNSUBSCRIBE [channel [channel ...]]
指退订给定的频道。
![45bbbe2d7d06fdb006b7be15bf398ce9.png](https://i-blog.csdnimg.cn/blog_migrate/00e0b858d3e28afd45ed36c5ba727e4c.png)
redis事务和watch命令
为什么要用事务?
redis的单个命令执行说原子性的,这个各位大佬都是了解的很滴。但是在多个命令的时候,就不是啦,所以需要一个事务,来把多个命令作为一个不可分割的处理序列,说白了就是这几个命令按顺序依次执行,中间又不会被其他客户端分开。
redis的事务有两个特点:
1:按进入队列顺序执行
2:不会受到其他客户端的影响
Redis 的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard (取消事务),watch(监视)
事务的用法
案例场景:a 和 b 各有 1000 元,a 需要向 b 转账 100 元。 a 的账户余额减少 100 元,b 的账户余额增加 100 元。
[root@instance-d26690z3 ~]# /opt/javaEvn/redisDir/bin/redis-cli
127.0.0.1:6379> auth 1
OK
127.0.0.1:6379> set a 1000
OK
127.0.0.1:6379> set b 1000
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY a 100
QUEUED
127.0.0.1:6379> INCRBY b 100
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 900
2) (integer) 1100
127.0.0.1:6379> MGET a b
1) "900"
2) "1100"
通过multi命令开启事务,multi 命令不能嵌套使用,如果已经开启了事务的情况下,再执行 multi
命令,会提示如下错误:
(error) ERR MULTI calls can not be nested
multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被 执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执 行。通过 exec 的命令执行事务。如果没有执行 exec,所有的命令都不会被执行。 如果中途不想执行事务了,怎么办? 可以调用 discard 可以清空事务队列,放弃执行。
watch命令
该命令可以用来实现乐观锁的一个行为。我们可以用watch监视一个或多个key,如果在开启事务后,有一个key在被监视,当执行事务exec之前该key值被其他的更改,那么整个事务都会被取消。
> watch k
OK
> multi
OK
> set k v2
QUEUED
> exec
(nil)
> get k
"v"
从以上命令可以看出,如果 exec
返回的结果是 nil
时,表示 watch
监控的对象在事务执行的过程中被其他修改了。从 get k
的结果也可以看出,在事务中设置的值 set k v2
并未正常执行。如果是正常执行应该返回的是 OK。
有个问题要注意, watch
命令只能在客户端开启事务之前执行,在事务中执行 watch
命令会引发错误,但不会造成整个事务失败,如下代码所示:
> multi
OK
> set k v3
QUEUED
> watch k
(error) ERR WATCH inside MULTI is not allowed
> exec
1) OK
> get k
"v3"
事务可能遇到的问题
在这呢,可以把事务执行过程中遇到的问题,分为两种。
- 在执行exec之前发生错误
比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set gupao 666
QUEUED
127.0.0.1:6379> hset qingshan 2673
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
- 在执行exec之后发生错误
比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> hset k1 a b
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"1"
最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下, 只有错误的命令没有被执行,但是其他命令没有受到影响。 这个显然不符合我们对原子性的定义,也就是我们没办法用 Redis 的这种事务机制来实现原子性,保证数据的一致。
Redis事务深入解析和WATCH使用_数据库_weixin_40663800的博客-CSDN博客blog.csdn.net![78c94150df6577a87791b0f7c3dfec9e.png](https://i-blog.csdnimg.cn/blog_migrate/b87eef7052e39d1995c6d211288932f3.png)
Lua脚本
Lua脚本,是一种轻量级的脚本语言。设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua脚本的应用也很多,比如Nginx+Lua实现的OpenResty,Redis+Lua配合使用(Redisson中大量使用了Lua脚本)。
在redis中调用lua脚本
使用 eval /ɪ'væl/ 方法,语法格式:
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
eval 代表执行 Lua 语言的命令。
lua-script 代表 Lua 语言脚本内容。
key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。
[key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。
[value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。
例如:返回一个字符串,0个参数:
redis> eval "return 'Hello World'" 0
在lua中调用redis命令
使用 redis.call(command, key [param1, param2…])进行操作。语法格式:
redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
command 是命令,包括 set、get、del 等。
key 是被操作的键。
param1,param2…代表给 key 的参数。
注意跟 Java 不一样,定义只有形参,调用只有实参。
Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。
设置键值对
在 Redis 中调用 Lua 脚本执行 Redis 命令
redis> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 test test
redis> get test
以上命令等价于 set test test。
在 redis-cli 中直接写 Lua 脚本不够方便,也不能实现编辑和复用,通常我们会把脚
本放在文件里面,然后执行这个文件。
在redis中调用lua脚本
创建 Lua 脚本文件:
cd /usr/local/soft/redis5.0.5/src
vim test.lua
Lua 脚本内容,先设置,再取值:
redis.call('set','test','lua666')
return redis.call('get','test')
在 Redis 客户端中调用 Lua 脚本
cd /usr/local/soft/redis5.0.5/src
redis-cli --eval test.lua 0
得到返回值:
[root@localhost src]# redis-cli --eval test.lua 0
"lua666"
Lua脚本具有以下好处:
1、减少网络开销:Lua脚本在执行的时候,是先发送到Redis服务器的,然后在服务器上执行脚本。
多个命令和业务逻辑都封装到脚本里,一次性提交到服务器。
2、原子性操作:我们都知道redis在执行命令时是单线程的,但是每个命令之间就存在并发的情况,
就存在先查询再操作时,两个命令没办法保证线程安全。但使用Lua脚本时,redis把这个脚本操作当成是一个命令,那么这个脚本中的多条操作也就保证了原子性。(注意:只保证原子性,不是事务)
虽然Lua脚本有这么多优点,但是也不能乱用,使用的时候要注意:
1、Lua脚本可以在redis单机模式、主从模式、Sentinel集群模式下正常使用,
但是无法在分片集群模式下使用。(脚本操作的key可能不在同一个分片)。
(其实集群模式不支持问题也是可以解决的,在使用spring的RedisTemplate执行lua脚本时,
报错EvalSha is not supported in cluster environment,不支持cluster。
但是redis是支持lua脚本的,只要拿到原redis的connection对象,通过connection去执行即可,
在后面会说下这个问题)
2、Lua脚本中尽量避免使用循环操作(可能引发死循环问题),尽量避免长时间运行。
3、redis在执行lua脚本时,默认最长运行时间时5秒,当脚本运行时间超过这一限制后,
Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),
而是会返回“BUSY”错误。
![bf472f93551a99d8da05298f8110087d.png](https://i-blog.csdnimg.cn/blog_migrate/7df9dd77c1ed3fd46c562523e1c41ba5.jpeg)
redis为什么这么快?
1.redis到底有多快?
我们也想知道redis到底有多快呢?其实redis自己是有提供工具可以测试一下滴。可以看下redis给的官方文档:
https://redis.io/topics/benchmarksredis.ioRedis包含的redis-benchmark
实用程序可模拟N个客户端同时发送M个总查询的运行命令根据官方的数据,Redis 的 QPS 可以达到 10 万左右(每秒请求数)。
执行命令: redis-benchmark -t set,lpush -n 100000 -q
结果(本地虚拟机):
SET: 49529.47 requests per second —— 每秒钟处理 5 万多次 set 请求
LPUSH: 49333.99 requests per second —— 每秒钟处理 5 万多次 lpush 请求
2.为什么这么快?
内存:kv结构的内存数据库,时间复杂度O(1)
单线程:没有多线程带来的上下文切换、竞争等问题
异步非阻塞:异步非阻塞 I/O,多路复用处理并发连接。
3.为什么塞单线程的?
因为单线程够用了,官方也是这么说的,CPU并不是使用Redis的瓶颈,
因为通常Redis要么受内存限制,要么受网络限制。
例如,使用在一般Linux系统上运行的流水线Redis每秒可以发送一百万个请求,
因此,如果您的应用程序主要使用O(N)或O(log(N))命令,则几乎不会使用过多的CPU 。
4.单线程为什么这么快?
1:因为单线程,避免了多线程的线程切换,就是上下文切换问题,其实这是耗资源的一个操作
2:I/O 多路复用,I/O 指的是网络 I/O。 多路指的是多个 TCP 连接(Socket 或 Channel)。
复用指的是复用一个或多个线程。
内存淘汰和过期策略
过期策略
Redis 中同时使用了惰性过期和定期过期两种过期策略。
1:定时过期(主动):
每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。
该策略可以立即清除过期的数据,对内存很友好;
但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
2:惰性过期(被动):
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。
该策略可以最大化地节省 CPU 资源,却对内存非常不友好。
极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。
例如 String,在 getCommand 里面
会调用 expireIfNeeded server.c expireIfNeeded(redisDb *db, robj *key)
第二种情况,每次写入 key 时,发现内存不够,
调用 activeExpireCycle 释放一部分内存。expire.c activeExpireCycle(int type)
3:定期过期:
每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,
可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
内存淘汰机制
如果内存满了,那该怎么办呢?所以就有了内存淘汰机制
最大内存设置
首先redis可以设置最大最大内存设置,redis.conf 参数配置:
# maxmemory <bytes>
如果不设置 maxmemory 或者设置为 0,64 位系统不限制内存,32 位系统最多使 用 3GB 内存。
也可命令行动态修改: redis> config set maxmemory 2GB
到达最大内存以后怎么办?提供内存淘汰机制,一共提供了八种内存淘汰机制
配置:redis.conf
# maxmemory-policy noeviction
(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,
新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据
不被丢失。
这八种大体上可以分为4中,lru、lfu、random、ttl。
持久化机制
RDB
RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
触发:
1:手动触发,save, shutdown, slave 命令会触发这个操作。
粒度比较大,如果save, shutdown, slave 之前crash了,则中间的操作没办法恢复。
2:配置规则触发,在配置文件中redis.conf中
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
save 300 10 # 400 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
# 以上满足任何一个都会触发
# 文件路径,
dir ./
# 文件名称
dbfilename dump.rdb
# 是否是 LZF 压缩 rdb 文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
过程:
fork一个进程,遍历hash table,利用copy on write,把整个db dump保存下来。
备份时:redis在进行数据持久化的过程中,创建子进程,会先将数据写入到一个临时文件中,待持久化过程都结束了,
才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总
是完整可用的。
优点:对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,
这样就确保了redis极高的性能。
恢复数据时:如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式
更加的高效。而且RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可,速度很快
虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,
因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。
所以,redis还提供了另一种持久化方式,那就是AOF。
AOF
AOF,英文是Append Only File,即只允许追加不允许改写的文件。
如前面介绍的,AOF方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍,
就这么简单。我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。
如果有写操作(如SET等),redis就会被追加到AOF文件的末尾。。
触发:自动触发
过程
默认的AOF持久化策略是每秒钟fsync一次(fsync是指把缓存中的写指令记录到磁盘中),
因为在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。
日志修复:如果在追加日志时,恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整,也没有关系,
redis提供了redis-check-aof工具,可以用来进行日志修复。
重写:因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,
为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,
redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,
但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
在进行AOF重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影
响AOF文件的可用性,这点大家可以放心。
虽然优点多多,但AOF方式也同样存在缺陷,比如在同样数据规模的情况下,AOF文件要比RDB文件的体积大。
而且,AOF方式的恢复速度也要慢于RDB方式。因为RDB是备份的数据,AOF是备份的写指令,需要重写执行
![89d99effc0ef1dbbcfe3189331e38f57.png](https://i-blog.csdnimg.cn/blog_migrate/e8c2047350141ac6ab73ec072c4a289b.jpeg)
![3ad7b77976b82792c25a8dd8dd2d3fe8.png](https://i-blog.csdnimg.cn/blog_migrate/18261757fc945d9af7fcd032ea9999ee.jpeg)
![70a5ecbc0dbc9c87528b1e768c189e9d.png](https://i-blog.csdnimg.cn/blog_migrate/8d0713b783b85879fffb9b698a044182.jpeg)
http://weixin.qq.com/r/myikvCvECnZ1rW-f932J (二维码自动识别)