赞
踩
摘抄自redis 官网:
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。
这意味着通常情况下一个请求会遵循以下步骤:
客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
服务端处理命令,并将结果返回给客户端。
因此,例如下面是4个命令序列执行情况:
Client: INCR X
Server: 1
Client: INCR X
Server: 2
Client: INCR X
Server: 3
Client: INCR X
Server: 4
客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。
这个时间被称之为 RTT (Round Trip Time - 往返时间). 当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个list,或者用很多Keys填充数据库)。例如,如果RTT时间是250毫秒(在一个很慢的连接下),即使服务器每秒能处理100k的请求数,我们每秒最多也只能处理4个请求。
如果采用loopback接口,RTT就短得多(比如我的主机ping 127.0.0.1只需要44毫秒),但它任然是一笔很多的开销在一次批量写入操作中。
幸运的是有一种方法可以改善这种情况。
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis很早就支持管道(pipelining)技术,因此无论你运行的是什么版本,你都可以使用管道(pipelining)操作Redis。下面是一个使用的例子:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
这一次我们没有为每个命令都花费了RTT开销,而是只用了一个命令的开销时间。
非常明确的,用管道顺序操作的第一个例子如下:
Client: INCR X
Client: INCR X
Client: INCR X
Client: INCR X
Server: 1
Server: 2
Server: 3
Server: 4
使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。
, 简单来说,redis使用管道技术,可以将多次请求合并成一次进行发送,其实就是各处可见的buffer的概念,可以提高传输速度。
有些时候,Redis实例需要装载大量用户在短时间内产生的数据(数据冷加载),数以百万计的keys需要被快速的创建。
使用正常模式的Redis 客户端执行大量数据插入不是一个好主意:因为一个个的插入会有大量的时间浪费在每一个命令往返时间上。使用管道(pipelining)是一种可行的办法,但是在大量插入数据的同时又需要执行其他新命令时,这时读取数据的同时需要确保请可能快的的写入数据。
只有一小部分的客户端支持非阻塞输入/输出(non-blocking I/O),并且并不是所有客户端能以最大限度的提高吞吐量的高效的方式来分析答复。
例如,如果我们需要生成一个10亿的`keyN -> ValueN’的大数据集,我们会创建一个如下的redis命令集的文件:
SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN
一旦创建了这个文件,其余的就是让Redis尽可能快的执行。在以前我们会用如下的netcat命令执行:
(cat data.txt; sleep 10) | nc localhost 6379 > /dev/null
然而这并不是一个非常可靠的方式,因为用netcat进行大规模插入时不能检查错误。从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。
使用pipe mode模式的执行命令如下:
cat data.txt | redis-cli --pipe
这将产生类似如下的输出:
All data transferred. Waiting for the last reply…
Last reply received from server.
errors: 0, replies: 1000000
使用redis-cli将有效的确保错误输出到Redis实例的标准输出里面。
但其实如果作为开发,这个工作可能也不会由我们亲手去做。
redis中,也是支持消息订阅的,有时候不一定非得用消息中间件来完成消息订阅,redis也可以。
看一下redis中关于消息订阅的相关指令:
127.0.0.1:6379> help @pubsub PSUBSCRIBE pattern [pattern ...] summary: Listen for messages published to channels matching the given patterns since: 2.0.0 PUBLISH channel message summary: Post a message to a channel since: 2.0.0 PUBSUB subcommand [argument [argument ...]] summary: Inspect the state of the Pub/Sub subsystem since: 2.8.0 PUNSUBSCRIBE [pattern [pattern ...]] summary: Stop listening for messages posted to channels matching the given patterns since: 2.0.0 SUBSCRIBE channel [channel ...] summary: Listen for messages published to the given channels since: 2.0.0 UNSUBSCRIBE [channel [channel ...]] summary: Stop listening for messages posted to the given channels since: 2.0.0
来尝试做个例子:
客户端1订阅c1频道:
127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
客户端2向c1频道推送消息:
127.0.0.1:6379> PUBLISH c1 balala
(integer) 1
127.0.0.1:6379> PUBLISH c1 gogogogo
(integer) 1
客户端1:
127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "message"
2) "c1"
3) "balala"
1) "message"
2) "c1"
3) "gogogogo"
当然,redis中的消息订阅与其他的消息订阅特性是一样的,订阅者只会接受到从自身订阅开始后,其他发布者发布的消息,以往的数据不被记录。
借着消息订阅,我们来聊一聊如果我们要设计一款在线聊天APP,对于消息的存储要如何处理?
对用户来说,它对查看消息的行为其实我们可以大致分为三类:实时消息,近期内(例如3天内)消息,和更长远历史记录。
这三种消息随着时间增长,看的人数会越来越少。结合这个特点我们可以这样设计:
对于实时消息,我们可以直接使用我们redis的消息订阅来实现。
对于近期消息,我们可以运用redis的存储,放入redis中的sorted_set中,为什么选用它呢? 因为sorted_set可以根据score进行排序,查看消息时天然支持的排序完美契合我们的需求。
而还有一点就是既然是近期的消息,我们自然需要定时去进行删除超过近期天数阈值的信息,而我们知道sorted_set是天然支持将某个score范围的数据进行删除的。此时我们恰好可以利用时间作为score,消息作为元素,就完成了这个比较麻烦的问题。
ZREMRANGEBYSCORE key min max
summary: Remove all members in a sorted set within the given scores
since: 1.2.0
#删除指定socre范围内的元素
对于更久远的数据,我们一定会有一个地方存储我们消息的全量数据,这个地方自然是数据库比较合适了。
既然决定好了设计,那怎么样的存放数据流程会比较合适呢?
如图,可以有多个客户端,除了发布和订阅的redis客户端之外,可以再多一个专门用于更新zset数据的订阅的redis客户端。
全量数据的存放,可以有一个微服务也作为一个redis客户端进行订阅,然后得到消息后,将消息流入到kafka,kafka再过度逐渐将数据流入数据库,这样降低了大量数据对数据库的压力。
接下来说一说redis的事务,说到事务,我们对熟悉的关系型数据库上的事务一定不陌生,首先的,最显著的一个特点就是将一组操作作为一个不可拆分的单元进行执行。
在redis中,其实事务是没有数据库那么强大的,它相对比较简单。 因为要记得,redis是单线程单进程来处理操作的。
事务相关指令:
127.0.0.1:6379> help @transactions DISCARD - summary: Discard all commands issued after MULTI since: 2.0.0 EXEC - summary: Execute all commands issued after MULTI since: 1.2.0 MULTI - summary: Mark the start of a transaction block since: 1.2.0 UNWATCH - summary: Forget about all watched keys since: 2.2.0 WATCH key [key ...] summary: Watch the given keys to determine execution of the MULTI/EXEC block since: 2.2.0
MULTI 命令用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。
另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。
以下是一个事务例子, 它原子地增加了 foo 和 bar 两个键的值:
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
使用事务时可能会遇上以下两种错误:
事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。
不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
在 Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。
至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
从协议的角度来看这个问题,会更容易理解一些。 以下例子中, LPOP 命令的执行将出错, 尽管调用它的语法是正确的:
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value
EXEC 返回两条bulk-string-reply: 第一条是 OK ,而第二条是 -ERR 。
—— Redis 不会停止执行事务中的命令。
以下例子展示的是另一种情况, 当命令在入队时产生错误, 错误会立即被返回给客户端:
MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command
因为调用 INCR 命令的参数格式不正确, 所以这个 INCR 命令入队失败。
如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
example:
客户端1:
127.0.0.1:6379> get k1
"2"
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k2 8
QUEUED
127.0.0.1:6379> set k3 9
QUEUED
127.0.0.1:6379> get k1
QUEUED
然后客户端2:
127.0.0.1:6379> del k1
(integer) 1
此时客户端1执行exec:
127.0.0.1:6379> exec
(nil)
发现因此k1被修改过,因此整个事务失效。
当客户端1和客户端2同时开启了事务,那是谁的先执行呢?一定是先调用exec的先执行。
像nginx一样,redis也是支持各种模块化扩展的,通过安装额外的模块使得reids功能更加丰富。
这里讲一下布隆过滤器的使用和为什么使用。
我们平时可能会把全量数据放入数据库,一些热点数据缓存在redis。
假设有人恶意高频率的去访问我们的系统,它访问的内容是我们redis中没有的,也是我们数据库没有的,这样一直访问,势必让我们系统的资源白白的空转浪费。
因此引入一些过滤器的存在,来阻挡住这些请求,这里我们要说一说的就是布隆过滤器。
首先我们要想进行拦截住,首先要知道我们到底有什么,没有什么,如果存储全量数据来标识有无,那成本太大了,而且作为redis作为缓存数据库,也不可能放入数据库的全量数据。
因此布隆过滤器在redis中的应用是借用bitmap,将一个元素内容进行多个映射函数,每个映射函数可以对应的bitmap的某一位。
这样,我们可以实现将我们的数据进行预先处理,将现有每一个数据进行标识,将对应的位标识为1。之后到达的某个请求,
将其内容经过多个函数如果映射到的位上都是1的话,说明请求的元素很有可能是存在的。当然也可能存在误标记导致放行,但这无疑的是,会大概率减少不存在数据的放行。
1、访问redis.io
2、点击modules
3、找到RedisBloom,访问RedisBloom的github:https://github.com/RedisBloom/RedisBloom
4、安装和启动
wget https://github.com/RedisBloom/RedisBloom/archive/v2.2.1.tar.gz
make
cp redisbloom.so /usr/local/redis5/
#重启redisserver后运行下面的命令
redis-server --loadmodule /usr/local/redis5/redisbloom.so
5、使用
#在布隆映射里添加存在的键值
127.0.0.1:6379> BF.ADD name zhangsan
(integer) 1
127.0.0.1:6379> BF.ADD name lisi
(integer) 1
#检验某个元素是否可以命中
127.0.0.1:6379> BF.EXISTS name zhangsan
(integer) 1
127.0.0.1:6379> BF.EXISTS name lisi
(integer) 1
#发现不存在的会返回0
127.0.0.1:6379> BF.EXISTS name balala
(integer) 0
过滤器可以了解的:
bloom(布隆过滤器)
counting bloom(布隆过滤器的升级)
cuckoo(布谷鸟过滤器)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。