《redis入门指南》之事务、缓存、排序、消息通知与空间节约

这一章节收获不少,其中最开眼界的是redis的强大的排序功能,以及如果信手拈来的事务功能。很多时候,技术本身没有问题,但是抽象的过于复杂,使得解决问题往往不是聚焦在问题本身,而是各类周边的工具。Redis在作为一个强有力的解决问题的工具方面,无疑是非常突出的。

事务

在我固有的印象中,事务是属于sql数据库才专有的特权,不过随着no-sql数据库的逐步成熟,这点确实有所变哈。

redis的事务功能,简单、直观,并且能够在实际的业务中发挥作用。一般的事务用法有两种:

  1. 使用Multi关键字包裹
    使用multi关键字,可以将需要一起执行的语句,保证同时发送到redis-server侧进行处理。如果说语句中存在语法错误,那么会全部终止(redis 2.6.5 以后)。
    但是如果在执行的时候出现了运行时错误,比如set了一个哈希map的key,那么这时候就无法进行回滚操作了,这点需要尤其注意。不过一般而言,这类错误应该在测试阶段就能够被发现并规避了。

这里举一个使用redis保存微博中的关注关系的实例:
每个用户都有一个关注的集合followingSet,对应的被关注的用户也有一个被关注的集合followedSet, 那么每次有关注关系发生的时候,绑定关系一定要是双向的:

$ret = $redis->multi()
    ->SADD("{$userIdA}:followingSet",userIdB)
    ->SADD("{$userIdB}followedSet",userIdA)
    ->exec();
  1. 使用watch关键字
    很多时候我们希望对一些独占的变量进行修改的过程中,不希望被打断,因为一旦变量的值被他人更改,很容易出现不一致的问题。这在一些抢占邀请码、领礼券等常见的业务场景中都有需求。所以这个时候一般就会使用watch与multi关键字结合。

举一个领礼券的例子:

// 假设有100个礼物 关注礼物数量这个key
$redis->watch("giftcnt");
// 用户领一个礼物,礼物数量减1,同时标识用户已经领过礼物了
$ret = $redis->multi()
    ->incrBy("giftcnt",-1)
    ->set("user:hasgift",1)
    ->exec();

假如说出现了静态条件,用户A在进行执行事务操作的时候,发现giftcnt已经不是自己获取的那个了,那么他的操作就需要终止,从而保证数量和表示状态的一致性。
Watch的思想,即是乐观锁,重试一次,希望不再遇到静态条件。一般事务成功的执行了之后,那么watch的过程也就完成了。

过期时间

在过期时间这个点上,最能体现redis作为缓存的功用。这也是我在深入了解redis之前唯一有概念的点。由于之前一直在用腾讯自研的CKV,其中用到最多的,确实也是这个expire的特性。

应该说,key的expire特性最能够体现大家使用缓存的普遍方式。那就是被动缓存,数据落地在mysql,同时缓存一份有过期时间的在redis,一旦过期之后,client再重新获取,继而写入到缓存中。一旦数据变更,则写入mysql,再写入缓存。
不过根据左耳朵耗子在博客中的分享,这种做法尽管能够满足一些常见的访问量不高的场景,但是在高并发的情况下是有问题的。设想client A访问cache,发现miss了,读取mysql,写入cache。但是与此同时,client B写入了mysql,而这个时间是在A已经把旧的数据灌入cache之后的。这就出现了问题。关于这个问题,不再深究,但是可以看看http://coolshell.cn/articles/17416.html 这篇文章。

redis中expire命令,针对string类型的键:
EXPIRE key seconds
PEXPIRE key miliseconds

也可以使用PERSIST/SET key命令把key恢复成永久。

从使用场景上面来看的话,比较常见的场景之一,就是实现访问的频率控制。针对唯一的用户+接口,我们希望限制他的访问频率在每分钟固定次数,比如100次。那么就可以通过如下的方式实现:

$listLength = LLEN rate.limiting:$IP
if $listLength < 100
    LPUSH rate.limiting:$IP, noew()
else 
    $time = LINDEX rate.limiting:$IP, -1 
    if now() - $time < 60
        print "访问频率超过限制"
    else 
        MULTI
        LPUSH rate.limiting:$IP, now()
        LTRIM rate.limiting:$IP, 0, 99   
        EXEC    

另外非常常见的场景,就是用作纯粹的缓存,这里需要注意两点:

  • 缓存时间的设置expire命令需要单独执行,并非原子操作,最好使用事务进行包装
  • 在用作缓存服务器的时候,必须限制redis使用的最大内存,并且使用一定的策略进行键值的淘汰,从而实现内存利用的最大化
    • maxmemory
    • memory-policy

排序

说老实话,在接触这部分内容之前,我以为redis的排序仅仅是使用有序集合来进行排列。但是其实redis内置了非常强大的SORT等命令,来完成各种排序,尤其是连接查询排序的功能。

如果我们相对包含了所有文章id的有序集合排序,那么就可以直接使用:
SORT articles:score DESC LIMIT 1 2
这样不但能够根据文章id的大小降序排列,还能limit返回的数量来方便翻页。

而对于更复杂的排序需求,则就需要使用BY关键字了。假如说,我们需要按照article的时间排序,那么就需要用到article的hashmap的结构了:
SORT articles:score BY article:*->time DESC
其中的*号就会被集合中的元素来替代,你可以理解成对articles:score做了一个foreach,然后被通配符*给替代了。这个用法确实令人耳目一新。

而除了使用哈希表中的元素,一个字符串类型的键同样也能被用作排序的key。

另外一个使用排序经常关心的问题,就是返回的结果的格式。按照上文中SORT的用法,会返回一系列排序好的文章id,然后需要手动的去hget其中的数据。而GET关键字就能够解决这个问题:

SORT articles:score BY article:*->time DESC GET article"*->title GET article"*->time GET #
#号表示获取id。

如果想把返回的结果以列表的形式存储,那么可以使用:
SORT articles:score BY article:*->time DESC GET article"*->title GET article"*->time GET # STORE sort.result

再配合EXPIRE命令,就可以进行缓存,从而保证SORT这类高耗时的操作不那么频繁的执行。

从性能来来讲,SORT的时间复杂度是O(n+mlog(m)), 同时redis也会提前建立一个长度为n的容器来存储待排序的元素。所以使用时一定注意:

  • 尽可能减少待排序键中元素的数量(减少N)
  • 使用LIMIT参数只获取需要的数据(减少M)
  • 充分利用STORE命令进行必要的缓存

消息通知

除了缓存,另外一个redis被流传最广的用法就是实现一个简易的队列。尽管从业界的使用来看,越来越专业的如rabbitMQ、rocketMQ、CMQ、KAFUKA等队列实现方案已经越来越成熟了。但是redis用作队列的简洁高效,仍然是很多队列处理能力要求不高的场景下的不二之选。

一个比较常见的场景是用户注册后,发送验证的邮件。那么定义一个队列 queue:verifymail,那么只需要开一个进程,阻塞的去等待队列,然后决定如何处理即可:
BRPOP queue:verifymail 0

但是有的时候,队列也会分优先级,比如找回密码的邮件和注册成功的通知邮件,其实优先级不同。所以可以进行优先级队列的阻塞等待:
BRPOP queue:findpassword queue:verifymail 0

除了实现队列任务之外,redis还可以用来实现不同频道的发布与订阅,从而实现后台任务更加顺畅的流转。
而对于发布者和订阅者的定义就是使用PUBLISH和SUBSCRIBE。而不同的频道则可以实现业务的隔离。

比如定义个一个频道是用户发评论,那么可能有积分服务、审核服务等都订阅了这个频道,那么在收到相应的通知之后,就可以进行相应的操作了,比如进行审核,或者是增加用户的积分等等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容