redis进阶使用
管道(Pipelining)
客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,
等待服务端响应。服务端处理命令,并将结果返回给客户端。
因此,例如下面是4个命令序列执行情况:
Client:INCR X
Server: 1
Client: INCR X
Server: 2
Client: INCR X
Server:3
Client: INCR X
Server: 4
———->上面的的这种方式,对于多级跳转有网络延迟的客户端,如果100K数据要求处理,每次都有网络延迟,那就会很慢——————–>于是redis有了pipelining管道
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis很早就支持管道(pipelining)技术,因此无论你运行的是什么版本,你都可以使用管道(pipelining)操作Redis。下面是一个使用的例子:
1 | $ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379 |
重要说明: 使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。
yum install nc
nc localhost 6379 //与redis建立socket通信
echo -e “ dadfaf \n dsadsad” 这个命令会识别\n换行符
1 |
|
管道(Pipelining) VS 脚本(Scripting)
大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。
应用程序有时可能在 pipeline 中发送 EVAL 或 EVALSHA 命令。Redis 通过 SCRIPT LOAD 命令(保证 EVALSHA 成功被调用)明确支持这种情况。
Pub/Sub发布与订阅
订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者)。而是发布的消息分到不同的频道,
不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的。
这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。
为了订阅foo和bar,客户端发出一个订阅的频道名称:
1 | SUBSCRIBE foo bar |
模式匹配结果的消息会以不同的格式发送:
- 消息类型是pmessage:这是另一客户端发出的PUBLISH命令的结果,匹配一个模式匹配订阅。第一个元素是原匹配的模式,第三个元素是原频道名称,最后一个元素是实际消息内容。
同样的,系统默认 SUBSCRIBE 和 UNSUBSCRIBE, PSUBSCRIBE 和 PUNSUBSCRIBE 命令在发送 psubscribe 和punsubscribe类型的消息时使用像subscribe 和 unsubscribe一样的消息格式。
同时匹配模式和频道订阅的消息
客户端可能多次接收一个消息,如果它订阅的多个模式匹配了同一个发布的消息,或者它订阅的模式和频道同时匹配到一个消息。就像下面的例子:
1 | SUBSCRIBE foo |
上面的例子中,如果一个消息被发送到foo,客户端会接收到两条消息:一条message类型,一条pmessage类型。
模式匹配统计的意义
在 subscribe, unsubscribe, psubscribe 和 punsubscribe 消息类型中,最后一个参数是依然活跃的订阅数。 这个数字是客户端订阅的频道和模式的总数。只有当退订频道和模式的数量下降到0时客户端才会退出Pub/Sub状态。
程序示例:
Peter Noordhuis 提供了一个使用EventMachine 和Redis创建多用户高性能网路聊天的很棒的例子。
客户端库实现提示
因为所有接收到的消息包含原订阅导致消息传递(message类型时是频道,pmessage类型时是原模式)客户端库可能绑定原订阅到回调方法(可能是匿名函数,块,函数指针),使用hash table。当消息被接收的时候可以做到时间复杂度为O(1)的查找以便传递消息到已注册的回调。
事务(tanslation)
开起事务mutli无论是谁先来的无所谓
只要看结束事务EXEC谁先到
谁就先处理事务
MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
EXEC 命令负责(结束事务就是拿出执行)触发并执行事务中的所有命令:
- 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。
- 另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。
当使用 AOF 方式做持久化的时候, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。
然而,如果 Redis 服务器因为某些原因被管理员杀死,或者遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。
如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。
使用redis-check-aof
程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。
从 2.2 版本开始,Redis 还可以通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作,具体信息请参考文档的后半部分。
用法
MULTI 命令用于开启一个事务,它总是返回 OK
。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个【队列中】, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。
另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。
以下是一个事务例子, 它原子地增加了 foo
和 bar
两个键的值:
1 | > MULTI |
EXEC 命令的回复是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 其中, 回复元素的先后顺序和命令发送的先后顺序一致。
当客户端处于事务状态时, 所有传入的命令都会返回一个内容为 QUEUED
的状态回复(status reply), 这些被入队的命令将在 EXEC 命令被调用时执行。
事务中的错误
使用事务时可能会遇上以下两种错误:
- 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用
maxmemory
设置了最大内存限制的话)。 - 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED
,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。
不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
在 Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。
至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
从协议的角度来看这个问题,会更容易理解一些。 以下例子中, LPOP 命令的执行将出错, 尽管调用它的语法是正确的:
1 | MULTI |
EXEC 返回两条bulk-string-reply: 第一条是 OK
,而第二条是 -ERR
。 至于怎样用合适的方法来表示事务中的错误, 则是由客户端自己决定的。
最重要的是记住这样一条, 即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行 —— Redis 不会停止执行事务中的命令。
以下例子展示的是另一种情况, 当命令在入队时产生错误, 错误会立即被返回给客户端:
1 | MULTI |
因为调用 INCR 命令的参数格式不正确, 所以这个 INCR 命令入队失败。
事务失败分为入队失败和执行失败
为什么 Redis 不支持回滚(roll back)
如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由【编程错误】(程序员逻辑,语法造成的失败)造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
放弃事务
当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出:
1 | > SET foo 1 |
使用 check-and-set 操作实现乐观锁
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
watch的意思就是事先监控某一个redis的key,保存这一个期望值,
处理时如果发现期望值与处理值对不上,那就取消事务,对的上就执行事务。
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
举个例子, 假设我们需要原子性地为某个值进行增 1 操作(假设 INCR 不存在)。
首先我们可能会这样做:
1 | val = GET mykey |
上面的这个实现在只有一个客户端的时候可以执行得很好。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 10 , 那么两个客户端都会将键的值设为 11 , 但正确的结果应该是 12 才对。
有了 WATCH , 我们就可以轻松地解决这类问题了:
1 | WATCH mykey |
使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 mykey
的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
了解 WATCH
WATCH 使得 EXEC 命令需要有条件地执行: 事务只能在所有被监视键都没有被修改的前提下执行, 如果这个前提不能满足的话,事务就不会被执行。 了解更多->
WATCH 命令可以被调用多次。 对键的监视从 WATCH 执行之后开始生效, 直到调用 EXEC 为止。
用户还可以在单个 WATCH 命令中监视任意多个键, 就像这样:
1 | redis> WATCH key1 key2 key3 |
当 EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。
另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
使用无参数的 UNWATCH 命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH 命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。
使用 WATCH 实现 ZPOP
WATCH 可以用于创建 Redis 没有内置的原子操作。举个例子, 以下代码实现了原创的 ZPOP 命令, 它可以原子地弹出有序集合中分值(score)最小的元素:
1 | WATCH zset |
程序只要重复执行这段代码, 直到 EXEC 的返回值不是nil-reply回复即可。
Redis 脚本和事务
从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。
因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。
不过我们并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。
不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的发生这种情况的话, 那么我们将废弃并最终移除事务功能。
bloom 过滤器
向上面那种是能够穿透缓存,让无用的元素进入数据库查找的,但是只要映射函数设计合理,这种几率是很少出现的
入果客户端提交的元素有一个映射有一个为0,那么说明数据库和redis缓存都没有,可以快速返回,不用进数据库
映射都为1,先进缓存找,缓存有,找到返回,缓存没有,然后进数据库
所以这种利用位图bitmap来标记那些元素数据库里有哪些没有,可以极大的控制没有的元素,不需要进数据库查找,直接在redis这层就过滤了
这就是bloom过滤器,用个几百MB的字节数组就可以标记成千上万甚至上数亿的元素
官方表示,虽然不是百分百的阻挡无用的请求元素,但是无用的元素进数据库查找的概率可以降低到小于1%,
所以bloom过滤器的使用:
1,数据库有啥
2把数据库里的元素标记到bitmap
3请求可能会误标记
4,但是穿透缓存概率大量减少,
5 他的成本低,只要一个合适大小的bitmap和一些合适的映射函数
对于架构师就有了几种方案
1客户端–实现bloom算法–并承载一个bitmap——->redis
2客户端–实现bloom算法——>redis—并承载一个bitmap
3客户端——>redis—-添加bloom算法模块—并承载一个bitmap—–这种更符合微服务的设计
https://github.com/RedisBloom/RedisBloom/archive/master.zip
linux对于zip压缩包
需要 yum install unzip
unzip xxx.zip
cd RedisBloom-master/
make
make完之后会出现redisbloom.so这么一个扩展库,与windows的后缀名为dll的文件一样的东西
把扩展库拷贝到redis的安装目录
cp redisbloom.so /opt/bigdata/redis5/
重点–:停掉redis
service redis_6379 stop
service redis_6380 stop
ps -ef | grep redis
回顾如果不是service这种服务启动redis实例
redis-server /etc/redis/6379.cof–这种需要手工把配置文件路径放进去,让redis-server启动程序 去读,然后开启一个redis实例
那么就可以
1 | redis-server --loadmodule /opt/bigdata/redis5/redisbloom.so |
1穿透缓存的解决方案,
如果数据库不存在,客户端必须发送添加key的指令,把value标记
2数据库更新了,要立即把新加的元素,添加到bloom过滤器
redis作为数据库/缓存的区别
缓存:
语义—->缓存数据不重要(数据库是有备份的),就是一致性要求低的数据,并且肯定不是全量数据,缓存应该随着访问而变化—–>所以放的就是热数据——->带来了一个问题—–>内存是有瓶颈的——->redis数据怎么随着业务变化,只保留热数据,因为内存大小有限
—引出的知识点——>
业务逻辑——->K的有效期
业务运转——–>因为内存有限—>随着访问的变化,应该淘汰掉冷数据
内存多大: vi /etc/redis/6379.conf
配置文件
1 |
|
1 | set k1 hello EX 20--->20秒后该key过期 |
过期原理:
Redis如何淘汰过期的keys
Redis keys过期有两种方式:被动和主动方式。
当一些客户端尝试访问它时,key会被发现并主动的过期。
当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。
具体就是Redis每秒10次做的事情:
- 测试随机的20个keys进行相关过期检测。
- 删除所有已经过期的keys。
- 如果有多于25%的keys过期,重复步骤1.
这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。
—————目的—–》稍微牺牲下内存,但是保住了redis的性能为王的这么一个策略
redis作为缓存:数据可以丢(根据业务逻辑)—>追求速度
redis作为数据库:数据是不能丢的—>速度+持久性–》内存是掉电易失
如果redis+mysql—-同时作为数据库处理业务(不太对)—>要考虑数据一致性问题
缓存常见问题,面试回答思路
击穿:
雪崩:
穿透:
一致性(双写):
redis的持久化
对于软件要存储都会有->
–快照/副本
–日志
单机自己:持久化
RDB—>
redisDB快照—>用快照的恢复起来时间快—>但是具有时点性就意味着操作日志不是实时的,可能每一个小时一次快照,可能半小时,但是总会有时间间隔,而这部分间隔的操作就会丢失
还有一个问题就是,八点开始写数据,假如要写到八点半,在这段时间内,有些数据在写之前发生改变,有些数据在写过之后发生改变,那最后写完的是哪一个时点的数据
插曲:linux的管道:ls -l /etc | more 前面作为输出,后面作为输入
((num++)) | echo ok —>num并没有++
管道:
1衔接,前一个命令的输出作为后一个命令的输入
2管道会触发创建一个【子进程】
echo $$ | more—->输出当前进程的id
echo $BASHPID | more—->输出子进程id号
$$优先级优于管道
那么管道会触发子进程—->
父进程的数据,子进程可不可以看的到
测试
1 | num=1 |
根据上面的推理,父进程导出数据给子进程,双方的修改都不影响对方,
那么redis作为父进程,创建一个子进程,父进程导出数据,让子进程去写快照。是可以得到一个正确时点的快照的—–>那么这种的解决方案的速度,和内存消耗怎么去给就是一个问题
操作系统有一个系统调用 ——>fork()—->速度快,消耗空间小
—-上图的文字描述不要看,说错了——
如果这样,父子进程操作同一个地址空间,那变量的值就会被修改
解决方案
copy on write
写入时才去复制
其实发生写入操作
无论父进程还是子进程
先在内存的一个新地址空间把数值写入
然后指针指向
copy不是真的copy,而是移动指针
有了上面的铺垫
redis拍快照,是通过创建子进程,创建子进程的形式时fork()形式,有了copy on write的系统机制,无论创建进程的速度还是内存的消耗,都是由极大的性能提高的
这样redis的子进程把数据写进磁盘成为一个快照,父进程即使修改,子进程也看不到,所以这样的快照才是一个时点的快照
这里fork()是系统调用,copyonwrite是系统机制,fork()是用来快速创建子进程–就是copy一份父进程的虚拟地址里的指针指向,然后放到子进程的虚拟地址里
copyonwrite是【系统机制】,可以让父子进程变相拥有隔离的数据,同时还能达到最小内存消耗
copyonwrite的实现—-例如:看内存地址为8的引用数,等于1,修改也就是write,不需要改变进程的虚拟地址的指针指向
内存地址引用大于1,进程的write需要写到其他的内存的地址空间,例如写到内存地址为9的空间,然后进程的虚拟地址指针指向内存地址9
操作:命令
1 |
|
弊端就是不支持拉链—永远只有一个dump.rdb快照—这是需要运维定时拷贝dump.rdb这个快照,并加上文件,
—这样dump.rdb只有写完,才能进行下次写
并且会有数据丢失,时点与时点之间窗口数据容易丢失
优点:类似java种的序列化,恢复速度相对txt文件来说要快,因为时字节形式存储的,体积小
AOF(append only file)
只会追加
redis的写操作,记录到文件中–实时的–丢失数据少
redis种 RDB和AOF可以同时开启—但是都开启的话redis只会拿AOF日志进行恢复—–4.0版本后—->AOF中包含rdb全量,增加新的写操作记录
如果只有AOF–优点肯定是数据完好,因为记录实时写进去的
缺点—恢复太慢,因为只追加,追加个10年的记录,那么恢复就得7–8年,并且文件体积大
RDB,AOF都有优缺点;
解决方案:
4.0以前——->重写—->删除抵消的命令,合并重复的命令——>最终也是一个纯指令的AOF日志文件
4.0以及4.0以后—->重写—–>将老的数据RDB写到AOF文件中,将增量记录的以指令的方式append追加到AOF—->也就是说新版AOF文件是一个RDB和AOF的整合
这就利用了RDB的快,和AOF的数据完整性
redis是内存数据库—–>对AOF写操作会触发I/O
由三个级别
NO
AWAYS
每秒:
1 |
|
验证知识点:
1 |
|
主从复制
- 本文作者: 忘忧症
- 本文链接: https://NepenthesZGW.github.io/2020/09/11/redis/redis应用/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!