事务
redis中的事务是一组命令的集合,事务同命令一样都是redis的最小执行单位。一个事务中的命令要么都执行,要么都不执行。事务的应用很普遍。事务的原理是先将属于一个事务的命令发送给redis,然后再让redis依次执行这些命令。
127.0.0.1:6379> multi
ok
127.0.0.1:6379> sadd "user:1:following" 2
queued
127.0.0.1:6379> sadd "user:2:following" 1
queued
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> smembers user:1:following
1) "2"
127.0.0.1:6379> smembers user:2:following
1) "1"
127.0.0.1:6379>
在如上所示:redis中使用multi命令告诉redis,下面将发给redis的命令属于同一个事务,先不要执行,而是把它们暂时存起来。而后发送了两个命令,redis并没有直接执行,而是返回queued,表示这两条命令已经进入等待执行的事务队列中,当把所有要在同一事务中执行的命令发送给redis后,使用exec命令告诉redis将等待执行的事务队列中的所有命令按照发送顺序依次执行。exec命令的返回值就是这些命令的返回值组成的列表,返回值顺序和命令的顺序相同。
redis保证一个事务中的所有命令要么都执行,要么都不执行,如果在发送exec命令前客户端断线了,则redis会情况事务队列,事务中的所有命令都不会执行,而一旦客户端发送了exec命令,所有的命令都会被执行,即使此后客户端断线也没关系。
redis的事务能保证一个事务内的命令一次执行,而不被其他命令插入。
在redis的事务中如果出现错误会有如下两种情况:
1、语法错误,是指命令不存在或者命令参数的个数不对。这种错误redis将直接告知该事务被中止,执行exec命令后,redis直接返回错误。
127.0.0.1:6379> multi
ok
127.0.0.1:6379> smembers <<----错误的命令
(error) err wrong number of arguments for 'smembers' command
127.0.0.1:6379> sadd "user:2:following" 1
queued
127.0.0.1:6379> exec
(error) execabort transaction discarded because of previous errors. <<---该exec无法正常的执行
127.0.0.1:6379>
2、运行错误,是指在命令执行时出现的错误,这种错误在实际执行之前redis无法发现,因此该错误在事务里会被redis接受并执行。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行。redis的事务没有关系数据库中提供的回滚功能,使用者必须在事务执行出错后处理对应的问题。
127.0.0.1:6379> multi
ok
127.0.0.1:6379> set key 1
queued
127.0.0.1:6379> sadd key 2
queued
127.0.0.1:6379> set key 3
queued
127.0.0.1:6379> exec
1) ok
2) (error) wrongtype operation against a key holding the wrong kind of value
3) ok
127.0.0.1:6379>
一个事务中只有当所有命令都一次执行完后才能得到每个结果的返回值,每个命令的执行结构都是最后一起返回的,无法将前一条命令的结果作为下一条命令的参数。
有些情况下需要先获得一条命令的返回值,再根据这个值执行下一条命令,此时可采用如下的策略,在获得键值后保证该键值不被其他客户端修改,直到函数执行完成后才允许其他客户端修改,防止竞争的产生。这可以通过事务家族中的watch成员来实现。watch可监控一个或多个键,一旦其中一个键被修改之后的事务就不会执行。监控一直持续到exec命令,事务中的命令是在exec之后执行,因此multi命令后可以修改watch监控的键值。
127.0.0.1:6379> set key 1
ok
127.0.0.1:6379> watch key
ok
127.0.0.1:6379> set key 2 <<---key被修改,因此事务不会被执行
ok
127.0.0.1:6379> multi
ok
127.0.0.1:6379> set key 3
queued
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get key <<---事务没有被执行
"2"
127.0.0.1:6379>
比较如下代码的差异:
127.0.0.1:6379> set key 1
ok
127.0.0.1:6379> set key 2
ok
127.0.0.1:6379> watch key
ok
127.0.0.1:6379> multi
ok
127.0.0.1:6379> set key 3
queued
127.0.0.1:6379> exec
1) ok
127.0.0.1:6379> get key <<---事务被执行
"3"
127.0.0.1:6379>
exec命令返回值是多行字符串类型,可以通过下标访问具体的结果。由于watch命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值。
执行exec命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用unwatch命令来取消监控,保证下一个事务的执行不受影响。
生存时间
在实际的开发中经常会遇到一些有时效性的数据,如缓存、验证码等,过了一定的时间就需要删除这些数据。在redis中可以使用expire命令设置一个键的生存时间,到时间后redis会自动删除它。
expire 命令的使用方法是: expire key seconds,其中的seconds参数表示键的生存时间,单位是秒。expire命令返回1表示设置成功,返回0则标识键不存在或设置失败。
127.0.0.1:6379> set session:ttl uid1214
ok
127.0.0.1:6379> expire session:ttl 900
(integer) 1
127.0.0.1:6379> del session:ttl
(integer) 1
127.0.0.1:6379> expire session:ttl 900
(integer) 0
127.0.0.1:6379>
可以使用ttl命令查看一个键还有多久时间会被删除,返回值是键的剩余时间。
127.0.0.1:6379> set foo bar
ok
127.0.0.1:6379> expire foo 20
(integer) 1
127.0.0.1:6379> ttl foo
(integer) 16
127.0.0.1:6379> ttl foo
(integer) -2
127.0.0.1:6379> get foo
(nil)
127.0.0.1:6379>
如果想取消键的生存时间设置,即将恢复成永久的,可以使用persist命令,如果生存时间呗成功清除则返回1,否则返回0(键不存在或键本来就是永久的),使用set或getset命令为键赋值也会同时清除键的生存时间。而其他只对键值进行操作的命令(incr、lpush、hset、zrem等)均不会映射键的生存时间。
expire命令的seconds参数必须是整数,最小单位是1s,想要更精确的控制键的生存时间必须使用pexpire命令。pexpire命令与expire的唯一 区别是前者的时间单位是毫秒,对应的pttl命令是以毫秒为单位返回键的剩余时间。
如果使用watch命令监测一个拥有生存时间的键,该键时间到期自动删除并不会被watch命令任务改建被改变。
127.0.0.1:6379> set foo bar1
ok
127.0.0.1:6379> expire foo 20
(integer) 1
127.0.0.1:6379> watch foo
ok
127.0.0.1:6379> multi
ok
127.0.0.1:6379> get foo
queued
127.0.0.1:6379> exec
1) (nil)
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> set foo bar1
ok
127.0.0.1:6379> expire foo 20
(integer) 1
127.0.0.1:6379> watch foo
ok
127.0.0.1:6379> multi
ok
127.0.0.1:6379> get foo
queued
127.0.0.1:6379> exec
1) "bar1"
127.0.0.1:6379>
为了提供网站的负载能力,长处将一些访问评论较高大对cpu或io资源消耗较大的操作的结果缓存起来,并希望让这些缓存过一段时间自动过期。但为了放置redis占用内存过大而将缓存键的生存时间设的太短,就可能导致缓存命中率过低并且大量内存白白的闲置。但由于很难为缓存键设置合理的生存时间,可以闲置redis能够使用的最大内存,并让redis按照一定的规则淘汰不需要的缓存键,这种方式将redis缓存系统时非常实用。具体的设置方法为:修改配置文件的maxmemory参数,闲置redis最大可用内存大小,当初超出这个限制时redis会依据maxmemory-policy参数指定的策略来删除不需要的键,直到redis占用的内存小于指定内存。
redis支持的淘汰键的规则:
volatile-lru 使用lru算法删除一个键(只对设置了生存时间的键)
allkeys-lru 使用lru算法删除一个键
volatile-random: 随机删除一个键(只对设置了生存时间的键)
maxmemory-policy支持的规则采用了lru算法,认为最少使用的键在未来一段时间内也不会被用到,即当需要空间时这些键可能被删除的。
消息通知
通知的过程可以借助任务队列来实现,任务队列就是传递任务的队列,任务队列由两类实体,一类是生产者,一类是消费者,生产者会将需要处理的任务放入任务队列中,而消费者则不断从任务队列中读入任务信息并执行。使用任务队列的好处如下:
1、松耦合,生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式,使得生产者和消费者可以由不同的团队使用不同的编程语言编写。
2、易于扩展消费者可以有多个,而且可以分布在不同的服务器中,借此可以降低单台服务器的负载。
队列很自然想到redis的列表类型,介绍了使用lpush和rpop命令实现队列的概念,要实现任务队列,只需要让生产者将任务使用lpush命令加入某个键中,另一边让消费者不断地使用rpop命令从该键中取出任务。但是该方案有个问题:当任务队列中没有任务时消费者需要周期的调用rpop命令查看是否有新任务。可以实现一旦有新任务加入任务队列就通知消费者,可借助brpop命令就可以实现具体的需求。brpop命令和rpop命令相似,唯一的区别是当列表中没有元素时brpop命令会一直阻塞主连接,直到有新元素加入。
brpop命令接收两个参数,第一个是键名,第二个是超时时间。单位是秒。当超过了此时间仍然没有获得新元素的话会返回nil。当超时时间为0时,表示不限制等待的时间,没有新元素加入列表就会永远阻塞下去。而当获得一个元素后brpop命令返回两个值,分别是键名和元素值。
[ ~]# redis-cli
127.0.0.1:6379> brpop queue 0
1) "queue"
2) "test"
(23.34s)
127.0.0.1:6379> brpop queue 0
1) "queue"
2) "gogo"
(7.99s)
127.0.0.1:6379>
[ ~]# redis-cli
127.0.0.1:6379> lpush queue test
(integer) 1
127.0.0.1:6379> lpush queue gogo
(integer) 1
127.0.0.1:6379>
除了brpop之外,redis还提供了blpop,只是从队列取元素的位置不同罢了。
brpop命令可以同时接收多个键,完整的格式为brpop key [key ...] timeout,意义是可以同时检测多个键,如果所有键都没有元素则阻塞,如果其中一个键有元素则会从该键中弹出元素。如果多个键都有元素,则按照从左到右的顺序取第一个键中的元素。如下的测试:
127.0.0.1:6379> lpush queue:1 task1
(integer) 1
127.0.0.1:6379> lpush queue:2 task2
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379> brpop queue:1 queue:2 0
1) "queue:1"
2) "task1"
127.0.0.1:6379> brpop queue:1 queue:2 0
1) "queue:2"
2) "task2"
127.0.0.1:6379> brpop queue:1 queue:2 0
1) "queue:1"
2) "task1"
127.0.0.1:6379> brpop queue:1 queue:2 0
1) "queue:2"
2) "task2"
127.0.0.1:6379>
根据上述的特性,即在同时有元素时按照由左到右的顺序取元素,因此可将需要优先执行的队列任务放在前面,这样就实现了优先级队列。
发布订阅模式
redis提供可一组命令可以让开发这实现发布订阅模式,该模式可以实现进程间的消息传递,基本原理如下:
发布订阅模式包含两种角色,分别是发布者和订阅者,订阅者可以订阅一个或若干个频道,而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
发布者发布消息的命令是publish,用法是publish channel message,publish命令的返回值表示接收到这条消息的订阅者数量。但因为发出去的消息不会被持久化,因此当客户端订阅了对应的频道后只能收到后续发布到该频道的消息,之前发送的无法接收到。
订阅频道的命令是subscribe,可同时订阅多个频道,用法是subscribe channel [channel ...],执行subscribe命令后客户端会进入订阅状态,处于该状态下客户端不能使用除了subscribe/unsubscribe/psubscribe/punsubscribe这4个属于发布订阅模式的命令之外的命令,否则会报错。
进入订阅状态后客户端可能受到三种类型的回复,每种类型的回复都包含3个值。第一个是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。消息类型可能的值有:
(1) subscribe, 表示订阅成功的反馈信息,第二个值是订阅成功的频道名称,第三个值是当前客户端订阅的频道数量。
(2) message, 这个类型的回复是最关心的,表示接收到的消息,第二个值是产生该消息的频道名称,第三个值是消息的内容。
(3) unsubscribe, 表示成功取消订阅某个频道,第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量。当该值为0时,客户端退出订阅状态。此时可以执行费发布订阅模式的命令啦。
127.0.0.1:6379> subscribe channel1 channel2
reading messages... (press ctrl-c to quit)
1) "subscribe" <<---订阅成功
2) "channel1"
3) (integer) 1
1) "subscribe" <<----订阅成功
2) "channel2"
3) (integer) 2
1) "message" <<----来自channel1的消息
2) "channel1"
3) "good-byte"
1) "message" <<---来自channel2的消息
2) "channel2"
3) "good-byte"
127.0.0.1:6379> publish channel1 good-byte
(integer) 1
127.0.0.1:6379> publish channel2 good-byte
(integer) 1
还可以使用psubscribe命令订阅指定的规则,支持glob分割通配符格式。
127.0.0.1:6379> psubscribe channel.?* <<---可以匹配channel.1和channel.10等。
reading messages... (press ctrl-c to quit)
1) "psubscribe" <<---订阅成功
2) "channel.?*"
3) (integer) 1
1) "pmessage" <<---对应的消息
2) "channel.?*" <<----订阅的频道通配符
3) "channel.1" <<----具体的频道
4) "hi!" <<----具体的消息内容
1) "pmessage"
2) "channel.?*"
3) "channel.10"
4) "hi!"
pmessage对应的四个成员分别是:第一个值表示这条消息是通过psubscribe命令订阅频道而收到的,第二个值表示订阅时使用的通配符,第三个值表示实际收到消息的频道命令,第四个值则是消息内容。
客户端和redis使用tcp协议连接,不论客户端向redis发送命令还是redis向客户端返回命令的执行结果,都需要经过网络传输,这两部分的总耗时称为往返时延,根据网络性能不同,往返时延也不同。如果执行较多的命令,每个命令往返时延累加起来对性能有一定影响。在执行多个命令时每条命令都需要等待上一条命令执行完才能执行,即使命令不需要上一条命令的执行结果。redis底层通信协议对管道提供了支持,通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中的每条命令都不依赖之前命令的执行结果是就可以将这组命令一起通过管道发出,管道通过减少客户端与redis的通信次数来实现降低往返延时累计值的目的。
阅读(3786) | 评论(0) | 转发(0) |