1、redis中的事务java
在关系型数据库中事务是必不可少的一个核心功能,生活中也是到处可见,好比咱们去银行转帐,首先须要将A帐户的钱划走,而后存到B帐户上,这两个步骤必须在同一事务中,要么都执行,要么都不执行,否则钱凭空消失了,换了谁也没法接受。redis
一样,redis中也为咱们提供了事务,原理是:先把一组同一事务中的命令发送给redis,而后redis进行依次执行。数据库
一、事务的语法:缓存
multi 命令1 命令2 ... exec
解释下语法:首先经过multi命令告诉redis:“下面我发给你的命令是属于同一事务的,你呢,先不要执行,能够把它们先存储起来”。redis回答:“okay啦”。然后咱们就发送银行转帐的命令1和命令2,这时redis将听从约定不会执行命令,而是返回queued,表示把这两条命令存储到等待执行的事务队列中了。最后咱们发送exec命令,redis开始依次执行在等待队列中的命令,完成一个事务的操做。性能优化
redis保证事务中全部命令要么全执行,要么都不执行。若是在发送exec前客户端断线了,redis会清空等待的事务队列,全部命令都不会执行。而一旦发送了exec,即便在执行过程当中客户端断线了也没有关系,由于redis早已存储了命令。session
下面咱们模拟下银行转帐的业务,从银行A转帐5000到银行B,中间企图修改银行A的余额,这时看看可否转帐成功并保证金额正确?post
127.0.0.1:6379> set bankA 10000 OK 127.0.0.1:6379> get bankA "10000" 127.0.0.1:6379> set bankB 10000 OK 127.0.0.1:6379> get bankB "10000"
127.0.0.1:6379> multi OK 127.0.0.1:6379> decrby bankA 5000 QUEUED 127.0.0.1:6379> incrby bankB 5000 QUEUED 127.0.0.1:6379>
127.0.0.1:6379> decrby bankA 10000 (integer) 0 127.0.0.1:6379> get bankA "0"
127.0.0.1:6379> exec 1) (integer) -5000 #这里假设余额能够透支,哈哈。变成﹣5000,而不是原来想的5000,若是实际业务,这时是没法进行转帐的。事务保证了余额正确 2) (integer) 15000
二、事务错误处理性能
若是在执行一个事务时,里面某个命令出错了,redis怎么处理呢?在redis中,须要分析致使命令错误的缘由,不一样的缘由会有不一样的处理方式。大数据
1)语法错误优化
语法错误是指该命令不存在或者参数个数不正确。对于这类错误,redis(2.6.5以后的版本)的处理方式是直接返回错误,所有不执行,即便里面有正确的命令。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set key value QUEUED 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379> iiiget key (error) ERR unknown command 'iiiget' 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get key (nil) #事务中有语法错误的命令,即便有一个命令正确也不会被执行 127.0.0.1:6379>
2)运行错误
运行错误是指命令在执行的时候报错,好比用散列的命令操做集合类型的键。这类错误在运行前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> get key "3"
可见sadd key 2出错了,可是set key 3依然被执行了。
redis中的事务不像关系型数据库有回滚机制,为此如出现这样的问题,开发者必须本身收拾形成这样的烂摊子了。
为了保证尽可能不出现命令和数据类型不匹配的运行错误,事前规划数据库(如保证键名规范)是尤其重要的。
三、watch命令
watch命令能够监控一个和多个键,一旦被监控键的值被修改,阻止以后的一个事务执行(即执行exec时返回nil,但这时watch监控也会失效),还记得上面转帐吗?当在转帐事务过程当中,bankA被取走了10000,余额变成0,这时操做转帐时应该提示余额不足,没法转帐。可使用watch命令来阻止转帐事务的执行。下面优化一下上面的转帐业务:
127.0.0.1:6379> watch bankA #监控银行A帐号 OK
127.0.0.1:6379> decrby bankA 10000
(integer) 0
127.0.0.1:6379> multi
OK 127.0.0.1:6379> decrby bankA 5000 QUEUED 127.0.0.1:6379> incrby bankB 5000 QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get bankA "0" 127.0.0.1:6379> get bankB "10000" 127.0.0.1:6379>
2、生存时间
在实际开发中常常会遇到一些有时效的数据,好比限时优惠活动、缓存或验证码等,过一段时间须要删除这些数据。在关系数据库中通常须要维护一个额外的字段来存储过时时间,而后按期检测删除过时数据。而在redis中命令就能够搞定。
一、命令
expire key seconds:返回1表示设置成功,0表示设置失败或该键不存在;
127.0.0.1:6379> set lifecycle 'test life cycle' OK 127.0.0.1:6379> get lifecycle "test life cycle" 127.0.0.1:6379> expire lifecycle 30 #设置生存时间为30秒,最小单位是秒 (integer) 1 127.0.0.1:6379> ttl lifecycle #ttl查看还剩多久 (integer) 13 127.0.0.1:6379> ttl lifecycle (integer) 11 127.0.0.1:6379> ttl lifecycle (integer) 9 127.0.0.1:6379> ttl lifecycle (integer) 8 127.0.0.1:6379> ttl lifecycle #当时间到了删除后会返回-2,不存在的键也返回-2 (integer) -2
取消设置时间:persist key
除了专用的取消命令,set,getset命令也会清除key的生存时间。
pexpire key mileseconds 精确到毫秒
3、排序
在咱们实际的开发中,不少地方用到排序这个功能,上节我们说过有序集合就能够实现排序功能,是经过它分数,redis除了有序集合外还有sort命令,sort命令很复杂,能用好它不是很容易,并且一不当心就可能致使性能问题,下面就说说这些排序。
一、有序集合排序
有序集合经常使用的场景是大数据排序,如游戏玩家的排行榜,通常不多须要得到键中的全部数据。
二、sort命令
除了上面的有序集合,redis还提供了sort命令,它能够对列表、集合、有序集合类型键进行排序,而且完成如关系数据库中关联查询相似的任务。
127.0.0.1:6379> sadd set_sort 5 2 1 0 7 9 (integer) 6
#不是说集合是无序的嘛,明明上面添加时无序的,这里打印出来怎么排序了呢?是这样的,集合经常用来存储对象的id,通常都是整数,对于这种状况,进行了优化,因此这里是排序了
127.0.0.1:6379> smembers set_sort 1) "0" 2) "1" 3) "2" 4) "5" 5) "7" 6) "9" 127.0.0.1:6379> sort set_sort desc #desc是按降序排序,固然这里的排序并不影响原始的数据 1) "9" 2) "7" 3) "5" 4) "2" 5) "1" 6) "0"
127.0.0.1:6379> lpush mylist 2 -1 3 44 5 (integer) 5 127.0.0.1:6379> lrange mylist 0 -1 1) "5" 2) "44" 3) "3" 4) "-1" 5) "2" 127.0.0.1:6379> sort mylist 1) "-1" 2) "2" 3) "3" 4) "5" 5) "44"
127.0.0.1:6379> zadd myzset 0 tom 1 helen 2 allen 3 jack (integer) 4 127.0.0.1:6379> zrange myzset 0 -1 #这是按照分数排序取得结果 1) "tom" 2) "helen" 3) "allen" 4) "jack" 127.0.0.1:6379> sort myzset (error) ERR One or more scores can't be converted into double 127.0.0.1:6379> sort myzset alpha #经过sort按照字母字典排序后的结果,这时忽略有序集合的分数了,按照元素的字典排序。 1) "allen" 2) "helen" 3) "jack" 4) "tom"
固然,若是结果集数据太多,须要分页显示,这时能够用limit参数搞定:
limit offset count 表示从第offset起,共取得count个数据
127.0.0.1:6379> sort myzset alpha desc limit 2 2 #表示从第二条取2条记录 1) "helen" 2) "allen"
三、sort命令的参数
1)By参数
有时咱们常常遇到对一个散列表中某个字段进行排序,好比写博客时按照文章的发布时间进行排序,取最新的文章放到首页,这个时候sort命令的by参数就能够派上用场了。
By参数的语法是:“by 参考键”,其中参考键能够是字符串类型或者散列类型键的某个字段(散列表示为:键名->字段名)若是提供了by参数,sort将不会再按照元素自身值进行排序,而是对每一个元素使用元素值替换参考键中的第一个‘*’并获取其值,而后依据该值进行排序。
有点绕啊,下面举例说明:假如java类别下有4篇博客文章,能够按照下图创建文章类别和文章的数据模型。具体类别和文章的结构图以下:
Tag:java:posts:表示文章的类别java,咱们用集合类型表示,里面只存储文章的ID
Post:id:表示文章,有三个字段,咱们用散列类型存储,一个id对应一个散列
这样模型能够这样创建了:
127.0.0.1:6379> hmset Post:1 title 'java study' content 'java is a good programming language' date '201509122110' OK 127.0.0.1:6379> hmset Post:22 title 'java study2' content 'java is a good programming language' date '201409121221' OK 127.0.0.1:6379> hmset Post:26 title 'java study3' content 'java is a good programming language' date '201709121221' OK 127.0.0.1:6379> hmset Post:60 title 'java study4' content 'java is a good programming language' date '201510221221' OK 127.0.0.1:6379> sadd Tag:java:posts 1 22 26 60 (integer) 4
这个时候我想对Tag:java:posts类别下的文章按照发布日期进行排序,能够这样来:
127.0.0.1:6379> smembers Tag:java:posts #排序以前直接取得的文章 1) "1" 2) "22" 3) "26" 4) "60" 127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc #按照日期排序后的结果,这里by后面参数是散列类型的 1) "26" 2) "60" 3) "1" 4) "22"
固然了,by后面还能够跟上字符串类型,以下:
127.0.0.1:6379> lpush sortbylist 2 1 3 (integer) 3 127.0.0.1:6379> set item:1 50 OK 127.0.0.1:6379> set item:2 90 OK 127.0.0.1:6379> set item:3 20 OK 127.0.0.1:6379> sort sortbylist by item:* desc 1) "2" 2) "1" 3) "3"
2)get参数
上面事例中的文章排序后,若是我想获取文章标题,须要针对每一个文章id进行hget,有没有以为很麻烦呢?其实sort还提供了get参数,能够很轻松的得到文章的标题
get参数不影响排序,它的做用是返回get参数指定的键值。get参数和by参数同样,也支持散列和字符串类型的键,并使用*做为占位符,要实现排序后直接返回文章的标题,能够这样作:
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title 1) "java study3" 2) "java study4" 3) "java study" 4) "java study2" 127.0.0.1:6379>
在一个sort中能够有多个get参数,可是by只能有一个,get #表示返回元素自己的值。还能够这样:
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get #
1) "java study3"
2) "201709121221"
3) "26"
4) "java study4"
5) "201510221221"
6) "60"
7) "java study"
8) "201509122110"
9) "1"
10) "java study2"
11) "201409121221"
12) "22"
3)store参数
若是但愿保存排序后的结果集,可使用store参数,默认保存后的数据类型是列表类型,若是该键已存在,则覆盖它。
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get # store sort.result (integer) 12 127.0.0.1:6379> lrange sort.result 0 -1 1) "java study3" 2) "201709121221" 3) "26" 4) "java study4" 5) "201510221221" 6) "60" 7) "java study" 8) "201509122110" 9) "1" 10) "java study2" 11) "201409121221" 12) "22" 127.0.0.1:6379>
store参数经常用来跟exprie命令实现排序结果的缓存功能,如上面提到的游戏排行榜数据,实现的伪代码以下
#判断是否存在排序结果的缓存 $isExistsCache = Exists cache.sort if ($isExistsCache = 1) #若是存在缓存,直接返回值 return lrange cache.sort 0 -1 else #若是不存在缓存,使用sort命令排序并将结果存入cache.sort缓存中 $sortResult=sort some.list store cache.sort #设置缓存的生存时间为10分钟 expire cache.sort 600 return $sortResult
四、性能优化
sort命令是redis中最强大复杂的命令之一,若是用很差很容易出现性能问题,sort命令的时间复杂度为:O(N+MlogM),其中N表示须要排序的元素个数,M表示返回的元素个数,当N数值很大时sort排序性能较低,而且redis在排序前会创建一个长度为N的容器来存储排序的元素,虽然是临时的,可是若是遇到大数据量的排序则会成为性能瓶颈。因此在开发中须要注意如下几个方面: