在分布式系统中,为了应对高并发的情况,有3种主要的手段 : 缓存,异步,分流

今天,我们要讲的就是在缓存中被最广泛使用的中间件 : Redis.

思维导图

Redis

Memcached vs. Redis?

为什么会有这个问题?

在大名鼎鼎的stackoverflow上搜索’redis',可以看到排名第一,最高赞的问答就是这个

search redis

这已经是2012年的问题了,虽然时过境迁,但这几年我在很多文章中还会看到对这个问题的讨论.

与Memcached的比较对于我们认识redis还是大有裨益的,可以更加了解redis的特色.

在这里就直接把一问一答都贴出来了.

question

名为Sagiv Ofek的用户需要在Memcached和Redis中做出抉择,他希望了解的是:

哪一个拥有更好的性能?Memcached和Redis彼此都有哪些优点,哪些缺点?比较的范围可以包括:

  • 读写的速率
  • 内存的使用
  • 持久化
  • 可拓展性

answer

最高赞Carl Zulauf的回答(最后更新于2017年)很全面了:

与memcached相比,Redis功能更强大,更受欢迎并且得到更好的支持.

Memcached只能做Redis可以做的一小部分。即使在两者功能重叠的部分,Redis也表现得更好.

对于任何新内容,请使用Redis。
  • 读/写速度两者都非常快。虽然基准测试因工作负载,版本和许多其他因素而异,但通常显示redis与memcached一样快或几乎一样快。我建议使用redis,但不是因为memcached速度慢。不是。
  • 内存使用 : Redis更好。
    • memcached:缓存大小由你决定,并且在插入数据时,守护程序会迅速增长到略大于该大小。除了重新启动memcached之外,从来没有真正的方法可以回收任何空间。您所有的密钥都可能过期,您可以刷新数据库,并且该数据库仍将使用您为其配置的全部RAM。
    • redis:设置最大大小由您决定。Redis将永远不会使用过多的内存,并会适时回收不再使用的内存
    • 我将100,000个〜2KB字符串(〜200MB)的随机句子存储到了两者中。Memcached RAM使用量增加到约225MB。Redis RAM使用量增加到〜228MB。刷新两次后,redis降至〜29MB,memcached保持在〜225MB。它们在存储数据方面同样有效,但是只有一个能够回收数据
  • 持久化:Redis显然是赢家,因为它默认情况下会这样做(自动持久化),并且具有很多配置项可选。而如果不借助第3方工具,Memcached没有任何机制可以转储到磁盘。
  • 可扩展性:在需要多个实例作为缓存之前,两者都为您提供了大量的空间。Redis包含的工具可帮助您超越此范围,而memcached则不会。(我补充一下,这里应该指的是Redis Replication,Redis Sentinel和Redis Cluster)

最后,他的结论是:

毫无疑问,对于任何新项目或尚未使用memcached的现有项目,我建议使用redis而非memcached。

以上听起来像我不喜欢memcached。相反:它是一个功能强大,简单,稳定,成熟和强化的工具。甚至在某些用例中,它比redis快一点。我喜欢memcached,我只是认为这对未来的发展没有多大意义。

在2020年的今天看来,二者不仅从理论上,而且从实践上也已经分出了高下.如今使用memcached的公司是少之又少了.

如果你的新项目考虑缓存,请毫不犹豫使用Redis.

应用场景

热点数据的缓存

由于redis访问速度快,支持的数据类型比较丰富,所以redis很适合用来存储热点数据,缓解DB的压力.

另外结合expire.我们可以设置过期时间然后再进行缓存更新操作.

限时业务

redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它.利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。

计数器

redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动,分布式序列号的生成.具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。

排行榜相关问题

榜单也属于热点数据,借助zset(sorted set)可以很轻松在redis上实现排行榜.

以用户的openid作为的username,以用户的点赞数作为的score, 然后针对每个用户做一个hash,通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息.

分布式锁

先用setnx来争抢锁,抢到以后用expire给锁加个过期时间,防止忘记释放锁而导致死锁.

由于setnx和expire两条命令的操作并非原子性,如果setnx之后执行expire之前进程挂掉,则可能造成死锁.

redis在2.6.12版本过后提供了解决方案:

命令:

SET key value [EX seconds] [PX milliseconds] [NX|XX]

例子:

SET key value ex 10 nx

过期时间是10,单位是秒(px则是毫秒).这样上面我们就用一条set指令原子性地替代了上面两条指令.

延时操作

这里同样借助zset来实现,但要注意,这里的’延时'不是指redis延迟发送数据(并不是mq),而是说客户端主动查询N秒前的数据.

拿时间戳作为score,消息内容作为key调用zadd生产消息,消费者用zrangebyscore命令获取N秒前的数据来轮询处理.

异步队列

看到有些文章提到有这个做法,个人觉得可以是可以(只要有双端list都能做),但是这个问题最好交给mq解决.

社交关系

set的特点是去重,因此用来表示交并集十分适合.

例如微博上的共同关注,共同好友,公众号上’xx位朋友也在看’等.

基本数据类型

Redis有5个基本的数据类型:string,hash,list,set,zset(sorted set).

当然,为了在内存上’精打细算',redis在它们之下还有ziplist,skiplist等数据结构,不过这部分内容会在另一篇文章中再详细研究.

  • String

    实用最广泛的数据类型,用途包括:

    • 缓存
    • 计数器,限速器
    • 共享session服务器也是基于该数据类型

    常用命令有:

    • set/get :set name时如果name重复,那么设置的值会进行覆盖

    • setnx : 如果这个name已经存在,不会进行覆盖而是直接返回0.可用于分布式锁,返回值1表示获取锁,返回值0表示获取失败,当发生死锁时可使用del命令将锁数据删除(需要检测是否超时).

    • setex : 设置该key在缓存中的过期时间,过了这个时间后返回nil

    • mset/mget : 批量设置与获取

    • incr/incrby/decr/decrby : 数值加减,用作计数器

  • Hash

    一个hash表下可包含多个field,常用于缓存结构化数据(如果用string的话需要序列化和反序列化等额外开销),例如数据库中的整张表(一个field对应表中的一个字段).

    hset user_1 id 1 name 张三 age 16 sex 1

    hset user_2 id 2 name 李四 age 16 sex 1

    这样如果需要修改用户的某个属性值,可以单个修改,例如:

    hset user_1 age 30

  • List

    特点是有序,用途主要包括:

    • 消息队列: LPUSH+BRPOP(阻塞特征)
    • 缓存: 用户记录等各种记录,最大特点是可支持分页
    • 栈: LPUSH+LPOP
    • 队列: LPUSH+RPOP
  • Set

    无序性,主要用于去重,交集可用于表示社交上的共同关注,共同爱好

  • Zset

    适合用于存储排行榜,例如消费排行,设置执行任务的权重

单线程模型

Redis是单线程的,为什么它会这么快?

另一个经典的问题了.

这可以分解成两个问题:

  1. 为什么redis是单线程的?

其实redis并非真正意义上的单线程服务器,之所以大家都这么认为可能是由于官方文档里的一句话:

“Redis is, mostly, a single-threaded server from the POV of commands execution (actually modern versions of Redis use threads for different things)."

“redis绝大部分的命令都是通过单线程执行,事实上现代版本的redis将多线程用于其他不同的事情上.”

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:

  • 多个套接字

  • IO多路复用程序

  • 文件事件分派器

  • 事件处理器

因为文件事件分派器队列的消费是单线程的(主要就是读写内存中的数据),所以Redis才叫单线程模型。

至于redis为什么是单线程的,官方的FAQ(http://www.redis.cn/topics/faq.html)是 :

因为CPU不是Redis的瓶颈。Redis的瓶颈最有可能是机器内存或者网络带宽.

既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

Redis客户端对服务端的每次调用都经历了发送命令执行命令返回结果三个过程。

其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。

如果CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办? 这很简单,单机多起几个Redis进程就好了。Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好。

  1. 为什么redis这么快?

Redis有多快可以参考官方的基准测试 : https://redis.io/topics/benchmarks

量级10W ~ 100W QPS.

主要的原因如下:

  • 基于内存,所以读写速度都很快.
  • 基于单线程,避免了上下文切换竞态产生的开销.
  • 使用epoll实现了I/O多路复用,不会因等待客户端发送数据而造成阻塞.