Redis中的事务(transaction)是一组命令的集合。 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。 一个事务从开始到执行会经历以下三个阶段: 开始事务。 命令入队。 执行事务。 以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
127.0.0.1:6379> multi OK 127.0.0.1:6379> set bookname "effect in java" QUEUED 127.0.0.1:6379> get bookname QUEUED 127.0.0.1:6379> sadd tag "b" "a" QUEUED 127.0.0.1:6379> smembers tag QUEUED 127.0.0.1:6379> exec 1) OK 2) "effect in java" 3) (integer) 2 4) 1) "Programming" 2) "b" 3) "Mastering Series" 4) "C++" 5) "a"如果一个事务中的某个命令执行出错,Redis会怎样处理呢?要回答这个问题,首先需要知道什么原因会导致命令执行出错。 (1).语法错误。
语法错误指命令不存在或者命令参数的个数不对。比如:
127.0.0.1:6379> mutli (error) ERR unknown command 'mutli' 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name "a" QUEUED 127.0.0.1:6379> set name2 (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379> setads (error) ERR unknown command 'setads' 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 跟在multi 命令后执行了3个命令:一个是正确的命令,成功地加入事务队列;其余两个命令都有语法错误。而只要有一个命令有语法错误,执行exec命令后Redis就会直接返回错误,连语法正确的命令也不会执行。 (2).运行错误 运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前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 可见虽然SADD key 2出现了错误,但是SET key 3依然执行了。 R edis的事务没有关系数据库事务提供的回滚( rollback)功能。为此开发者必须在事务执行出错后自己收拾剩下的摊子(将数据库复原回事务执行前的状态等)。 不过由于Redis不支持回滚功能,也使得Redis在事务上可以保持简洁和快速。另外回顾刚才提到的会导致事务执行失败的两种错误,其中语法错误完全可以在开发时找出并解决,另外如果能够很好地规划数据库(保证键名规范等)的使用,是不会出现如命令与数据类型不匹配这样的运行错误的。如果想取消键的生存时间设置(即将键恢复成永久的),可以使用persist命令。如果生存时间被成功清除则返回1,否则返同0(因为键不存在或键本来就是永久的).
127.0.0.1:6379> expire school 20 (integer) 0 127.0.0.1:6379> persist school (integer) 0 127.0.0.1:6379> ttl school (integer) -2 使用SET或getset命令为键赋值也会同时清除键的生存时间,例如: 127.0.0.1:6379> expire school 20 (integer) 0 127.0.0.1:6379> set school haida OK 127.0.0.1:6379> ttl school (integer) -1 expire命令的seconds参数必须是整数,所以最小单位是1秒。如果 想要更精确的控制键的生存时间应该使用pexpire命令,两者的唯一区别是pexpire的时间单位是毫秒,即pexpire key 1000与EXPIRE key 1等价。对应地可以用PTTL命令以毫秒为单位返回键的剩余时间。 提示如果使用WATCH命令监测了一个拥有生存时间的健,该键时间到期自动删除并不会被WATCH命令认为该健被改变。-------------------------------------------------------------------------------------------------------------------------------
集合类型提供了强大的集合操作命令,但是如果需要排序就要用到有序集合类型。Redis的作者在设计Redis的命令时考虑到了不同数据类型的使用场景,对于不常用到的或者在不损失过多性能的前提下可以使用现有命令来实现的功能,Redis就不会单独提供命令来实现。这一原则使得Redis在拥有强大功能的同时保持着相对精简的命令。 有序集合常见的使用场景是大数据排序,如游戏的玩家排行榜,所以很少会需要获得键中的全部数据。同样Redis认为开发者在做完交集、并集运算后不需要直接获得全部结果,而是会希望将结果存入新的键中以便后续处理。这解释了为什么有序集合只有zinterstore和zunionstore命令而没有zinter和zunion命令。 当然实际使用中确实会遇到需要直接获得集合运算结果的情况,除了等待Redis加入相关命令,我们还可以使用mulit,zinterstore,zrange,del和exec这5个命令自己实现zinter: mutil zinterstore tempKey ... zrange tempKey ... del tempKey exec 3-1.sort命令 sort命令可以对列表类型、集合类型和有序集合类型键进行排序,并且可以完成与关系数据库中的连接查询相类似的任务。如"ruby"标签的文章的ID分别是"2","6","12",使用smembers命令不能获取有序的结果。可以用sort命令实现
redis>sort tag:ruby:posts 1)"2" 2)"6" 3)"12"除了集合类型,sort命令还可以 对列表类型和有序集合类型进行排序: 127.0.0.1:6379> lpush mylist 4 2 6 (integer) 3 127.0.0.1:6379> sort mylist 1) "2" 2) "4" 3) "6" 127.0.0.1:6379> sort mylist desc 1) "6" 2) "4" 3) "2" 在对 有序集合类型排序时 会忽略元素的分数,只针对元素自身的值进行排序。例如: 127.0.0.1:6379> zadd myzset 50 a 20 b 40 c (integer) 3 127.0.0.1:6379> sort myzset (error) ERR One or more scores can't be converted into double 127.0.0.1:6379> zadd myzset1 50 1 60 3 70 2 (integer) 3 127.0.0.1:6379> sort myzset1 1) "1" 2) "2" 3) "3" 除了可以排列数字外,sort命令还可以通过 alpha参数实现 按照字典顺序排列非数字元素,就像这样: 127.0.0.1:6379> lpush mylistalpha a c b (integer) 3 127.0.0.1:6379> sort mylistalpha (error) ERR One or more scores can't be converted into double 127.0.0.1:6379> sort mylistalpha alpha 1) "a" 2) "b" 3) "c" 从这段示例中可以看到如果没有加ALPHA参数的话,SORT命令会尝试将所有元素转换成双精度浮点数来比较,如果无法转换则会提示错误。 那么如果数量过多需要 分页显示呢?sort命令还支持 limit参数来返回指定范围的结果。用法和SQL语句一样,limit offset count,表示跳过前offset个元素并获取之后的count个元素。 SDRT命令的参数可以组合使用,像这样: 127.0.0.1:6379> sort mylist desc 1) "6" 2) "4" 3) "2" 127.0.0.1:6379> sort mylist desc limit 1 2 1) "4" 2) "2"GET参数不影响排序,它的作用是使sort命令的返回结果不再是元索自身的值,而是GET参数中指定的键值。GET参数的规则和BY参数一样,GET参数也支持字符串类型和散列类型的键,并使用“*”作为占位符。 在一个sort命令中可以有多个GET参数(而BY参数只能有一个)。可见有N个GET参数,每个元素返回的结果就有N行。 GET # 会返回元素本身的值。
默认情况下sort会直接返回排序结果,如果希望保存排序结果,可以使用store参数。 store参数常用来结合expire命令缓存排序结果.
sort是Redis中最强大最复杂的命令之一,如果使用不好很容易成为性能瓶颈.srot命令的时间复杂度是O(n+mlogm),其中,表示要排序的列表(集合或有序集合)中的元素个数,m表示要返回的元素个数。当n较大的时候sort命令的性能相对较低,并且Redis在排序前会建立一个长度为n的容器来存储待排序的元素,虽然是一个临时的过程,但如果同时进行较多的大数据量排序操作则会严重影响性能。 所以开发中使用SORT命令时需要注意以几点: (1)尽可能减少待排序键中元素的数量(使n尽可能小)。 (2)使用limit参数只获取需要的数据(使m尽可能小)。 (3)如果要排序的数据数量较大,尽可能使用store参数将结果缓存。
---------------------------------------------------------------------------------------------------------------------
通知的过程可以借助任务队列来实现。任务队列顾名思义,就是“传递任务的队列”。与任务队列进行交互的实体有两类,一类是生产者(producer),一类是消费者(consumer)。生产者会将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读入任务信息并执行。 对于发邮件这个操作来说页面程序就是生产者,而发邮件的进程就是消费者。当需要发送邮件时,页面程序会将收件地址、邮件主题和邮件正文组装成一个任务后存入任务队列中。同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其从队列中取出并执行。由此实现了进程间的通信。 使用任务队列有如下好处。 (1)松耦合。生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。 (2)易于扩展消费者可以有多个,而且可以分布在不同的服务器中,如图。借此可以轻易地降低单台服务器的负载。
Redis的列表类型使用lpush和lpop命令实现队列。如果要实现任务队列,只需要让生产者将任务使用LPUSH命令加入到某个键中,另一边让消费者不断地使用RPOP命令从该键中取出任务即可。 完成发邮件的任务需要知道收件地址、邮件土题和邮件正文。所以生产者需要将这二个信息组成对象并序列化成字符串,然后将其加入到任务队列中口而消费者则循环从队列中拉取任务,就像如下伪代码:
# 无限循环读取任务队列中的内容 loop $task=RPOP queue if $task # 如果任务队列中有任务则执行它 execute($task) e1se # 如果没有则等待1秒以免过于频繁地请求数据 wait 1 second 到此一个使用Redis实现的简单的任务队列就写好了。不过还有点不完美的地方:当任务队列中没有任务时消费者每秒都会调用一次RPOP命令查看是否有新任务。如果可以实现一旦有新任务加入任务队列就通知消费者就好了。其实借助BRPOP命令就可以实现这样的需求。 BRPOP命令和RPOP命令相似,唯一的区别是 当列表中没有元素时BRPOP命令会一直阻塞住连接,直到有新元素加入.如上段代码可改写为: loop # 如果任务队列中没有新任务,BRPOP命令会一直阻塞,不会执行execute() $task=BRPOP queue, 0 # 返回值是一个数组,数组第二个元素是我们需要的任务 execute($task[1]) BRPOP命令接收两个参数,第一个是键名,第二个是超时时间,单位是秒。当超过了此时问仍然没有获得新元素的话就会返回nil。上例中超时时间为“O”,表示不限制等待的时间,即如果没有新元素加入列表就会永远阻塞下去。 当获得一个元素后BRPOP命令返回两个值,分别是键名和元素值。 为了测试BRPOP命令,我们可以打开 两个redis-cli实例,在实例A中: 127.0.0.1:6379> brpop queue 0 键入回车后实例1会处于阻塞状态,这时在实例B中向queue中加入一个元素; 127.0.0.1:6379> lpush queue task (integer) 1 在LPUSH命令执行后实例A马上就返回了结果; 127.0.0.1:6379> brpop queue 0 # 结果会马上返回 1) "queue" 2) "task" (4.66s) 同时会发现实例B中的queue中的元素已经被取走: 127.0.0.1:6379> llen queue (integer) 0 除了BRPOP命令外,Redis还提供了BLPOP和BRPOP的区别在与从队列取元素时BLPOP会从队列左边取,这里不再赘述。当发送确认邮件和发送通知邮件两种任务同时存在时,应该优先执行前者。为了实现这一目的,我们需要实现一个优先级队列。 BRPOP命令可以同时接收多个键,其完整的命令格式为BLPOP key [key…」timeout,如BLPOP queue:l queue:2。意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素。例如,打开两个redis-cli实例,在实例A中: 127.0.0.1:6379> blpop queue:1 queue:2 queue:3 0 在实例B中: 127.0.0.1:6379> lpush queue:2 task (integer) 1 则实例A中会返回: 1) "queue:2" 2) "task" (9.76s) 如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。
# 先在queue:2和queue:3中各加入一个元素 127.0.0.1:6379> lpush queue:2 task1 (integer) 1 127.0.0.1:6379> lpush queue:3 task2 (integer) 1 # 执行brpop命令 127.0.0.1:6379> brpop queue:1 queue:2 queue:3 0 1) "queue:2" 2) "task1"除了实现任务队列外,Redis还提供了一组命令可以让开发者实现“发布/订阅”(publish/subscribe)模式。“发布/订阅”模式同样可以实现进程间的消息传递,其原理是这样的: “发布/订阅”模式中包含两种角色,分别是发布者(pub)和订阅者(sub)。订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
发布者发布消息的命令是publish,用法是 publish channel message. 以下实例演示了发布订阅是如何工作的。在我们实例A中我们创建了订阅频道名为 redisChat:
redis 127.0.0.1:6379> SUBSCRIBE redisChat Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "redisChat" 3) (integer) 1 现在,我们先重新开启个 redis 客户端B,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。 127.0.0.1:6379> publish redisChat "one new" (integer) 2 127.0.0.1:6379> publish redisChat "two new" (integer) 2 # 订阅者的客户端A会显示如下消息 1) "message" 2) "redisChat" 3) "one new" 1) "message" 2) "redisChat" 3) "two new" 在客户端A取消订阅某个频道 127.0.0.1:6379> unsubscribe redisChat 1) "unsubscribe" 2) "redisChat" 3) (integer) 0 执行subscribe命令后客户端会进入订阅状态,处于此状态下客户端不能使用除subscribe/unsubscribe/psubscribe/punsubscribe这4个属于“发布/订阅”模式的命令之外的命令(后面3个命令会在下面介绍),否则会报错。 进入订阅状态后客户端可能收到三种类型的回复。每种类型的回复都包含3个值,第一个值是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。消息类型可能的取值有: (1) subscribe表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个值是当前客户端订阅的频道数量。 (2) message。这个类型的回复是我们最关心的,它表示接收到的消息。第一个值表示产生消息的频道名称,第三个值是消息的内容。 (3) unsubscribe表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量,当此值为D时客户端会退出订阅状态,之后就可以执行其他非“发布/订阅”模式的命令了。 除了可以使用subscribe命令订阅指定名称的频道外,还可以使用P SUBSCRIBE命令订阅指定的规则。规则支持glob风格通配符格式,下面我们新打开一个redis-cli,实例C进行演示:127.0.0.1:6379> psubscribe redisChat?*-----------------------------------------------------------------------------------------------------------------------
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤: 1).客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。 2).服务端处理命令,并将结果返回给客户端。 客户端和Redis使用TCP协议连接。不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延。根据网络性能不同,往返时延也不同,大致来说到本地回环地址(loop back address)的往返时延在数量级上相当于Redis处理一条简单命令(如LPUSH list 1 2 3)的时间。如果执行较多的命令,每个命令的往返时延累加起来对性能还是有一定影响的。 在执行多个命令时每条命令都需要等待上一条命令执行完(即收到Redis的返回结果)才能执行,即使命令不需要上一条命令的执行结果。如要获得post:l,post:2和post:3这3个键中的title字段,需要执行三条命令,如图所示。 Redis的底层通信协议对管道(pipelining )提供了支持。通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中每条命令都不依赖于之前命令的执行结果时就可以将这组命令一起通过管道发出。管道通过减少客户端与Redis的通信次数来实现降低往返时延累计值的目的,如图所示。之后会结合不同的编程语言介绍如何在开发的时候使用管道技术。
相比于硬盘而言,内存在今天仍然显得比较昂贵。而Redis是一个基于内存的数据库,所有的数据都存储在内存中,所以如何优化存储,减少内存空问占用对成本控制来说是一个非常币要的话题。 1).精简键名和键值 精简键名和键值是最直观的减少内存占用的方式,如将键名very.important.person:20改成VIP:20。当然精简键名一定要把握好尺度,不能单纯为了节约空间而使用不易理解的键名(比如将VIP:20修改为V:20,这样既不易维护,还容易造成命名冲突)。又比如一个存储用户性别的字符串类型键的取值是male和female,我们可以将其修改成m和f来为每条记录f节约几个字节的空间(更好的方法是使用0和1来表示性别,稍后会详细介绍原因)。 2).内部编码优化 有时候仅凭精简键名和键值所减少的空间并不足以满足需求,这时就需要根据Redis内部编码规则来节省更多的空间。Redis为每种数据类型都提供了两种内部编码方式,以散列类型为例,散列类型是通过散列表实现的,这样就可以实现O(1)时间复杂度的查找、赋值操作,然而当键中元素很少的时候,O(1)的操作并不会比O(n)有明显的性能提高,所以这种情况下Redis会采用一种更为紧凑但性能稍差(获取元素的时间复杂度为O(n)的)的内部编码方式。内部编码方式的选择对于开发者来说是透明的,Redis会根据实际情况自动调整。当键中元素变多时Redis会自动将该键的内部编码方式转换成散列表。如果想查看一个键的内部编码方式可以使用object encoding命令.
