你可能不知道的Redis用法

0. 引言

基于Redis丰富的数据结构,除了充当缓存层来提升查询效率以外,还能应用在很多常见的场景,比如:分布式锁,消息队列,限流等。看到这些场景你可能会有疑问,Redis在这些领域好像并不出名啊,比如消息队列,出名的有Rocketmq、rabbitmq等等,很少听Redis来做这个场景,是不是存在什么问题?是的,下面的文字就来总结下Redis在这些场景的常规用法以及存在的问题。

1. 分布式锁

1.1 基本使用

分布式应用通常会遇到并发问题,逻辑上我们可以使用setnx指令占一个“坑”,然后处理自己的业务逻辑,最后再调用del指令释放“坑”。

> setnx lock.test true
OK
... do something ...
> del lock.test
(integer) 1

以上是常规使用,会有个问题,如果逻辑执行过程出现异常,导致没有执行到del指令,最后会陷入死锁。

1.2 过期时间

因此,更进一步的做法是拿到锁以后,再给锁设置一个过期时间,这样当过程出现异常,没有执行del指令,锁也会在5s后自动释放。

> setnx lock.test true
OK
> expire lock.test 5
... do something ...
> del lock.test
(integer) 1

1.3 原子性问题

实现以上逻辑后,仍存在问题,比如在执行setnx指令之后,但在expire指令时服务器出现异常,没有给锁设置上过期时间,锁依然会陷入死锁的状态,一直不会释放。

如何解决呢?可能你会想到事务,但在这里不行,因为expire是依赖于setnx的执行结果的,如果没有抢到锁,expire不应该被执行。事务里没有if-else的逻辑,要么全部执行,要么一个都不执行。

在Redis 2.8 版本中,作者加入了set指令的扩展参数,使得setnxexpire指令可以一起执行。

> set lock.test true ex 5 nx
OK
... do something ...
> del lock.test

上面的指令就是setnxexpire组合在一起的原子指令。

1.4 超时问题

Redis分布式锁并不解决锁超时的问题,所以不建议在获取分布式锁后处理耗时较长的逻辑。因为逻辑执行得太长,锁到期自动释放,就会出现问题。

有一个稍微安全点的方案:在抢锁时,set指令的value参数设置为一个随机数,释放锁时先匹配value是否一致,再进行删除key。这种方式可以确保当前连接的操作,不会被其他连接释放,除非是过期自动释放。

以上的匹配value和删除key不是原子性的,所以需要使用lua脚本,来保证连续多个指令的原子性执行。但是这也不是一个完美的方案,只是相对安全一点。它始终没能解决锁超时,其他线程“乘虚而入”的问题。

2. 消息队列

2.1 基本使用

基于Redis的list数据结构,利用lpushrpop的指令组合,可以模拟队列。

image.png

使用的代码就不贴了,逻辑比较简单。下面讨论两个个问题:

  1. 队列空了怎么办?
  2. Redis主动断开空闲连接怎么处理?

队列空了怎么办?
rpop返回空时,sleep(1000)。可以这么做,但是这导致消费的延迟,Redis提供了更好的方案:阻塞读(blpop/brpop),用这个指令替代逻辑里的rpop即可。

Redis主动断开空闲连接怎么处理?
使用了阻塞读以后,线程会一直阻塞在那里,如果一直没有数据,这个连接就会成了闲置连接,如果时间过久,Redis会主动断开连接,从而减少闲置资源占用。此时blpop/brpop会抛出异常,所以客户端需要捕捉该异常,并重试。

2.2 延迟队列

最近有个业务需求:当某个行为触发了,则在10s后执行一段逻辑。

看到「10s后执行」这种典型的场景,个人的第一反应便是延迟队列。在Redis中,可以通过(zset)有序集来实现。将消息序列化为value,将执行时间作为score,然后轮询zset获取到期的任务进行处理。

多进程同时消费的场景中,Redis的zrem方法是关键,通过zrem来决定唯一的属主,它的返回值决定了是否有抢到任务。

进一步优化

使用lua脚本,将zrangebyscorezrem操作一同发送到服务端执行,可以减少争抢任务时的浪费。

2.3 消息多播

上面讨论的是Redis作为消息队列的基本使用,实际情况Redis仍有很多不足,其中一个就是它不支持多播机制。

消息多播是指生产者生产一次消息,由中间件将消息复制到多个消息队列,每个队列都有相应的消费者进行消费。


image.png

2.3.1 PubSub

为了支持多播,Redis引入了新的模块去支持:PubSub,即发布者/订阅者模式。如何使用这里就不说了,文档很详细。下面总结下缺点:

  1. 如果一个消费者都没有的情况下,消息会直接丢弃;
  2. 如果消费者连接断开了,当它重连上以后,断开期间的消息会丢失;
  3. 如果Redis宕机,PubSub消息不会持久化,消息直接丢弃;

2.3.2 Stream

Redis 5.0 新增了一个数据 Stream,它是一个抢到的支持多播的可持久化消息队列,作者坦言它极大地借鉴了Kafka的设计。

Stream的消费模型借鉴了Kafka的消费分组的概念,弥补了PubSub不能持久化消息的缺陷。Stream又不同于Kafka,Kafka可以分Partition,而Stream不行。

3. 位图

给个场景:记录用户一年的签到情况,签到为1,没签为0。

如果用key/value的方式存,一年365天,当用户量上亿,所需要的存储空间是惊人的。为了解决这一问题,Redis提供了位图数据结构,上面的场景(可以引申存储bool型数据的其他场景),每天的签到记录只占1个位,365个位对应46个字节,大大节省存储空间。

3.1 基本使用

位图不是特殊的数据结构,它的内容其实就是普通字符串,也就是byte数组。对字符串的指令get/set,是对整个内容的操作,而对其中的位操作Redis提供了getbit/setbit的指令。

字符“he”的ASCII码与位的对应关系:


image.png

通过位操作设置“he”字符:


image.png

3.2 统计与查找

除了设置和获取位图的值以外,Redis还提供了bitcountbitpos分别用于统计和查找。比如:

  1. 通过bitcount统计用户一共签到了多少天,可指定范围[start, end];
  2. 通过bitpos查找用户从那一天开始第一次签到,可指定范围[start, end];

但遗憾的是,start和end参数是字节索引,也就是指定的位范围必须是8的倍数,而不能任意指定。因此,我们无法直接计算某个月内用户签到了多少天。
具体操作就不说了,看文档就好。

4. 布隆过滤器

通过位图来节省空间,谈到这种方式,怎么能不谈布隆过滤器。布隆过滤器是什么,以及原理这里就不说了,只说跟Redis相关的。

Redis官方提供的布隆过滤器到了Redis 4.0 提供了插件功能才正式登出。两个基本指令,bf.addbf.exists。如果需要一次添加多个,就需要使用到bf.madd,同样的,一次查询多个元素是否存在,就需要用到bf.mexists指令。

什么时候用布隆过滤器呢?
判断某个值存在,会出现误判;判断某个值不存在,100%准确。基于这个特性去思考,就很容易找到使用场景啦,比如:

  1. 爬虫系统对URL去重,爬过的网页不爬;
  2. 邮箱系统的垃圾邮件过滤功能;
  3. NoSQL数据库,常用布隆过滤器过滤掉不存在的row,减少数据库的IO请求数量。

如何控制低误判率?
Redis中提供了bf.reserve指令,可设置key,error_rate和initial_size,设置的error_rate越低,需要的空间越大。

5. 附近的店/人/车

Redis 3.2 版本以后增加了地理位置Geo模块,可以实现类似摩拜单车的“附近的车”、美团和饿了么的“附近的餐馆”这样的功能。

位置数据通常使用二维的经纬度表示,经度范围[-180, 180],纬度范围[-90, 90]。试想下如果使用关系型数据库存储(元素 id, 经度 x, 纬度 y),该如何计算?
假设(x0, y0)是用户,r是半径,使用一条SQL就可以圈出来。

select id from positions where x0-r < x < x0+r and y0-r < y < y0+r;
image.png

可以对经纬度坐标加上索引进行优化,但数据库查询性能毕竟有限,可能不是一个很好的方案。

业界比较通用的地理位置距离排序算法是GeoHash算法,它是将二维的经纬度数据映射到一维的整数。映射的算法和用法这里就不具体展开了。下面是两个使用注意事项:

  1. 一维映射是有损的
    在使用Redis的Geo查询时,时刻想着它的内部结构实际上是一个zset(skiplist)。通过zset的score排序就可以得到坐标附近的其他元素,通过score还原成坐标值就可以得到元素的原始坐标。但需要注意,通过映射再还原回来的值会出现较小的差别,原因是二维坐标进行一维映射是有损的。

  2. Geo数据单独Redis实例部署更加
    Redis的Geo数据结构,数据会全部放到一个zset集合中。如果在Redis集群环境,集合可能从一个节点迁移到另一个节点,如果单个key的数据过大,会对集群迁移工作造成较大影响。因此,Geo的数据建议使用单独Redis实例部署,不适用集群环境。

6. 限流

6.1 简单限流

限流的场景非常常见,控制用户行为,如发帖、回复、点赞等。简单的限流策略:限定用户的某个行为在指定的时间内只允许发生n次,这里我们可以使用zset数据结构的score值,存储毫秒时间戳,就可以很方便的取某个时间窗口内用户的行为次数。


image.png

此方案有个缺点,因为它要记录时间窗口内所有的行为记录,如果这个量很大,此方案就不合适了,因为会消耗大量的存储空间。

6.2 漏斗限流

Redis 4.0 提供了一个限流Redis模块,叫Redis-Cell。该模块使用了漏斗算法,并提供了原子的限流指令。指令只有一条:cl.throttle,对着文档来使用即可。

7. 总结

上面的描述没有深入过多的技术细节,重点还是以讨论场景为主,因为据个人的了解,其实很大一个部分开发者对Redis的认识还只是作为缓存层,但基于Redis的丰富数据结构,Redis可以在很多场景中发挥作用。结合场景再思考其数据结构设计,也能有所感悟。后面准备再整理一篇关于Redis数据结构以及内存优化的总结(希望能完成吧哈哈)。

参考书籍:

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