赞
踩
Redis使用单reactor的非阻塞I/O多路复用机制,采用单线程串行处理命令,且线程同时处理命令和网络IO。
串行:表现在单CPU上,多个线程排好队,依次执行。
并行:表现在多CPU上,每个CPU处理一个线程,多个线程可以同时执行同一段代码。
并发:表现在单CPU上:一个CPU交替执行多个线程;
表现在多CPU上:多个CPU处理多个线程,并行一定是并发。
Redis 执行一条命令需要发送命令
、命令排队
、命令执行
、结果响应
四个步骤,而且Redis是基于 Request/Response
协议(停等机制)的,客户端连接采用阻塞IO的方式,即:一个redis客户端连接,发出请求后,必须收到响应才能发起下一次请求。
当需要批量处理命令的时候,使用上面的方式会极大降低效率。
pipeline是由客户端提供的,它能将一组 Redis 命令进行组装,通过一次传输给 Redis 并返回结果集。如下是Redis批量处理命令的过程比对:
Redis pipeline注意点:
Pipeline
是非原子的,在上面原理解析那里已经说了就是 Redis 实际上还是一条一条的执行的,而执行命令是需要排队执行的,所以就会出现原子性问题。Pipeline
中包含的命令不要包含过多。Pipeline
每次只能作用在一个 Redis 节点上。Pipeline
不支持事务,因为命令是一条一条执行的。Redis的命令是原子性的,虽然Redis支持事务,但是redis 的事务是非原子性的。使用MULTI 开启事务,事务执行过程中,单个命令是入队列操作,直到调用 EXEC 才会一起执行。
Redis事务相关命令:
Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
例子:事务实现 加倍操作
- WATCH score:10001
- val = GET score:10001
- MULTI
- SET score:10001 val*2
- EXEC
Redis常见的事务失败是由语法错误或者数据结构类型错误导致的:
演示"语法错误"的事务执行结果:(结果表明k1、k2保留原值)
先令:
- set name liming
- set age 20
然后按下图中的过程执行:
演示"数据类型错误"的事务执行结果:(结果表明k1值被修改,k2保留原值)
先令:
- set name liming
- set age 20
然后按下图中的过程执行:
语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,因此Redis不支持事务回滚。
Redis还使用lua脚本实现了事务的原子性。如果项目中使用了lua脚本,不需要使用上面的事务命令。
Redis中加载了一个lua虚拟机;用来执行redis lua脚本;redis lua 脚本的执行是原子性的;当某个 脚本正在执行的时候,不会有其他命令或者脚本被执行;lua脚本当中的命令会直接修改数据状态;
Lua广泛作为其它语言的嵌入脚本,尤其是C/C++,语法简单,小巧,源码一共才200多K,这可能也是Redis官方选择它的原因。Nginx也支持Lua,利用Lua也可以实现很多有用的功能。
Lua在Redis脚本中我个人建议只需要使用下面这几种类型:
nil
空boolean
布尔值number
数字string
字符串table
表- --- 全局变量
- name = 'felord.cn'
- --- 局部变量
- local age = 18
Redis脚本在实践中不要使用全局变量,局部变量效率更高。
- local a = 10
- if a < 10 then
- print('a小于10')
- elseif a < 20 then
- print('a小于20,大于等于10')
- else
- print('a大于等于20')
- end
Redis中使用EVAL
命令来直接执行指定的Lua脚本。通常在测试脚本的时候使用这种方式。
- EVAL luascript numkeys key [key ...] arg [arg ...]
-
- EVAL:命令的关键字。
- luascript:Lua 脚本。
- numkeys:指定的Lua脚本需要处理键的数量,其实就是 key数组的长度。
- key:传递给Lua脚本零到多个键,空格隔开。在Lua 脚本中通过KEYS[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys。
- arg:传递给脚本的零到多个附加参数,空格隔开。在Lua脚本中通过ARGV[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys。
接下来我简单来演示获取键hello
的值得简单脚本:
- 127.0.0.1:6379> set hello world
- OK
- 127.0.0.1:6379> get hello
- "world"
- 127.0.0.1:6379> EVAL "return redis.call('GET',KEYS[1])" 1 hello
- "world"
- 127.0.0.1:6379> EVAL "return redis.call('GET','hello')" 0
- "world"
从上面的演示代码中发现,KEYS[1]
可以直接替换为hello
,但是Redis官方文档指出这种是不建议的。
numkeys无论什么情况下都是必须的命令参数。
Redis还提供了evalsha命令来执行Lua脚本,首先要把Lua脚本加载到redis服务端,得到该脚本的sha1校验码,evalsha命令使用sha1作为参数直接执行缓存在服务器中对应的Lua脚本。这样避免了每次发送Lua脚本的开销,客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
将脚本缓存到服务器的操作可以通过 SCRIPT LOAD 命令进行。
- 127.0.0.1:6379> EVALSHA sha1 numkeys key [key ...] arg [arg ...]
-
- sha1 : 通过 SCRIPT LOAD 生成的 sha1 校验码。
- numkeys: 用于指定键名参数的个数。
- key [key ...]: 跟EVAL执行时的参数key一样
- arg [arg ...]: 跟EVAL执行时的参数arg一样
使用示例如下:
- 10.19.132.41:1>script load 'local val = KEYS[1]; return val'
- "0eed5a3c55921764489eb93f0e1f978c3c28f394"
- 10.19.132.41:1>evalsha "0eed5a3c55921764489eb93f0e1f978c3c28f394" 1 kkkkkk
- "kkkkkk"
在上面的例子中我们通过redis.call()
来执行了一个SET
命令,其实我们也可以替换为redis.pcall()
。它们唯一的区别就在于处理错误的方式。
由于在Redis中存在Redis和Lua两种不同的运行环境,在Redis和Lua互相传递数据时必然发生对应的转换操作,这种转换操作是我们在实践中不能忽略的。例如如果Lua脚本向Redis返回小数,那么会损失小数精度;如果转换为字符串则是安全的。
- 127.0.0.1:6379> EVAL "return 3.14" 0
- (integer) 3
- 127.0.0.1:6379> EVAL "return tostring(3.14)" 0
- "3.14"
Lua脚本在Redis中是以原子方式执行的,在Redis服务器执行EVAL
命令时,在命令执行完毕并向调用者返回结果之前,只会执行当前命令指定的Lua脚本包含的所有逻辑,其它客户端发送的命令将被阻塞,直到EVAL
命令执行完毕为止。因此LUA脚本不宜编写一些过于复杂了逻辑,必须尽量保证Lua脚本的效率,否则会影响其它客户端。如果lua脚本内成功执行部分命令后就失败了退出了,则成功执行的命令对数据的修改会被保留下来。
SCRIPT LOAD:加载脚本到缓存以达到重复使用,避免多次加载浪费带宽,每一个脚本都会通过SHA校验返回唯一字符串标识。需要配合EVALSHA
命令来执行缓存后的脚本。
- 127.0.0.1:6379> SCRIPT LOAD "return 'hello'"
- "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
- 127.0.0.1:6379> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0
- "hello"
SCRIPT FLUSH:既然有缓存就有清除缓存,但是遗憾的是并没有根据SHA来删除脚本缓存,而是清除所有的脚本缓存,所以在生产中一般不会再生产过程中使用该命令。
SCRIPT EXISTS:以SHA标识为参数检查一个或者多个缓存是否存在。
- 127.0.0.1:6379> SCRIPT EXISTS 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 1b936e3fe509bcbc9cd0664897bbe8fd0cac1012
- 1) (integer) 1
- 2) (integer) 0
SCRIPT KILL:终止正在执行的脚本。但是为了数据的完整性此命令并不能保证一定能终止成功。如果当一个脚本执行了一部分写的逻辑而需要被终止时,该命令是不凑效的。需要执行SHUTDOWN nosave
在不对数据执行持久化的情况下终止服务器来完成终止脚本。
使用lua脚本的注意事项:
function
函数,整个脚本作为一个函数的函数体。local
关键字。key
分到相同机器,也就是同一个插槽(slot)中,可采用Redis Hash Tag技术。 redis发布订阅(pub/sub)是一种消息通信模式
,消息的发布者
不会将消息发送给特定的订阅者
,而是通过消息通道(频道
)广播出去,让订阅该消息主题(频道)的订阅者消费。发布/订阅模式的最大特点是利用消息中间件,实现解耦。
redis的发布订阅又分为两类:
当使用发布订阅功能时需要重启开启一个连接,而非使用命令连接:因为命令连接严格遵循请求回应模式;而发布订阅连接需要一直接收redis主动推送的内容。所以实际项目中如果支持pubsub的话,需要另开一条连接用于处理发布订阅:
1、使用subscribe
命令指定当前客户端订阅频道,一个订阅者可以订阅多个频道,若该频道不存在则会创建。
- 语法:
- subscribe channel channel2 : 订阅一个或多个频道
-
- 测试:
- 10.19.132.41:0>subscribe shen-channel1 shen-channel2
- Switch to Pub/Sub mode. Close console tab to stop listen for messages.
- 1) "subscribe" --返回值类型:订阅者
- 2) "shen-channel1" --订阅频道名称
- 3) "1" --订阅成功与否
2、使用publish
命令指定当前客户端向某个频道发布消息。
- 语法:
- publish channel message : 向channel频道发送message消息
-
- 测试:
- 10.19.132.41:0>publish shen-channel1 11
- "1" --接收到此消息的订阅者数量,无订阅者返回0
- 10.19.132.41:0>publish shen-channel2 helloworld
- "1"
3、当发布者发送消息后,订阅者会接收到消息
- 1) "message" --返回值类型:消息
- 2) "shen-channel1" --接收的频道名
- 3) "11" --消息内容
-
- 1) "message"
- 2) "shen-channel2"
- 3) "helloworld"
4、使用pubsub
命令可以查看频道的基本信息
- 语法:
- pubsub channels : 查看当前存在的所有频道
- pubsub numsub channel : 查看指定频道的订阅者数量
5、使用unsubscribe
命令可以指定当前客户端退订1个或多个频道
- 语法:
- unsubscribe channel1 channel2 :退订频道
-
- 测试:
- 10.19.132.41:0>unsubscribe shen-channel1
- 1) "unsubscribe" --返回类型:退订
- 2) "shen-channel" --退订的频道名
- 3) (integer) 1
在redisServer中有一个字典类型字段叫pubsub_channels,用来保存订阅信息,key为频道,value为订阅该频道的客户端:
- struct redisServer{
- pid_t pid;
- //...
- // 保存所有频道订阅关系
- dict *pubsub_channels;
- //...
- }
1、使用psubscribe
命令进行模式订阅
- 语法:
- psubscribe pattern-1 pattern-2 :订阅1个或多个模式频道
-
- 测试:
- 10.19.132.41:0>psubscribe shen*
- Switch to Pub/Sub mode. Close console tab to stop listen for messages.
- 1) "psubscribe" --返回类型:模式订阅
- 2) "shen*" --订阅模式名称 shenxxx
- 3) "1" --订阅成功与否
2、仍然使用publish
命令指定当前客户端向某个频道发布消息
- 10.19.132.41:0>publish shen-channel2 helloworld
- "3"
3、当发布者发送消息后,订阅者会接收到消息
- 1) "pmessage"
- 2) "shen*"
- 3) "shen-channel2"
- 4) "helloworld"
4、使用punsubscribe
命令可以指定当前客户端退订1个或多个模式
- 语法:
- punsubscribe pattern-1 pattern-2 : 退订1个或多个模式频道
-
- 测试:
- 10.19.132.41:0>punsubscribe shen*
- 1) "punsubscribe"
- 2) "shen*"
- 3) "0"
在redisServer中有一个链表字段叫pubsub_patterns,该链表保存着所有和模式相关的信息
- struct redisServer {
- //...
- list *pubsub_patterns;
- // ...
- }
-
- typedef struct pubsubPattern {
- client *client; -- 订阅模式客户端
- robj *pattern; -- 被订阅的模式
- } pubsubPattern;
发布订阅的生产者传递过来一个消息,redis会直接找到相应的消费者并传递过去;假如没有消费者,消息直接丢弃;假如开始有2个消费者,一个消费者突然挂掉了,另外一个消费者依然能收到 消息,但是如果刚挂掉的消费者重新连上后,在断开连接期间的消息对于该消费者来说彻底丢失 了;
总结:
1、解耦多个应用服务,如聊天室
2、Redisson的分布式锁的实现就采用了发布订阅模式:获取锁时,若获取不成功则订阅释放锁的消息,在收到释放锁的消息前阻塞,收到释放锁的消息后再去循环获取锁。
3、异步处理:采用Redis的发布订阅模式来实现异步处理,从而提高并发量。如:秒杀功能可以这样做:
- 1、秒杀之前,将产品的库存从数据库同步到Redis
- 2、秒杀时,通过lua脚本保证原子性
- (1)扣减库存
- (2)将订单数据通过Redis的发布订阅功能发布出去
- (3)返回1(表示成功)
Redis底层使用的通信协议是RESP(Redis Serialization Protocol的缩写),RESP协议实际上是一个支持以下数据类型的序列化协议:Simple Strings(简单字符串),Errors(错误),Integers(整形),Bulk Strings(块字符串)和Arrays(数组),但此协议只适用于Redis客户端-服务端之间的通信,Redis集群中节点间通信使用的另一种协议。
在RESP协议中,每种数据数据类型都有固定的前缀,每一部分都以\r\n结尾
在Redis中,RESP用作 请求-响应 协议的方式如下:
1、客户端将命令作为批量字符串的RESP数组发送到Redis服务器。
2、服务器(Server)根据命令执行的情况返回一个具体的RESP类型作为回复。
例如:
客户端执行set name liming命令,客户端会将其序列化为:
- *3\r\n$3\r\nset\r\n$4\r\nname\r\n$6\r\nliming\r\n
-
- 解释:
- *3:表示长度为3的数组
- \r\n:特殊分隔符
- $3:set的长度
- $4:name的长度
服务端收到请求进行处理后,返回的响应是:
+OK\r\n
简单字符串和批量字符串的区别:简单字符串一般是服务器状态相关,比如'OK'、‘PONG’等;而Bulk Strings可以包含任何内容(比如换行符、控制符)
同步连接方案采用阻塞io来实现;优点是代码书写是同步的,业务逻辑没有割裂;缺点是阻塞当前线程,直至redis返回结果;通常用多个线程来实现线程池来解决效率问题;
异步连接方案采用非阻塞io来实现;优点是没有阻塞当前线程,redis没有返回,依然可以往redis 发送命令;缺点是代码书写是异步的(回调函数),业务逻辑割裂,可以通过协程解决 (openresty,skynet);配合redis6.0以后的io多线程(前提是有大量并发请求),异步连接池,能更好解决应用层的数据访问性能;
说明:redis6.0版本后添加的 io多线程主要解决redis协议的压缩以及解压缩的耗时问题;一般项目中不 需要开启;如果有大量并发请求,且返回数据包一般比较大的场景才有它的用武之地;
参考文献:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。