在分布式系统中,为了应对高并发的情况,有3种主要的手段 : 缓存,异步,分流
今天,我们要讲的就是在缓存中被最广泛使用的中间件 : Redis.
思维导图
Memcached vs. Redis?
为什么会有这个问题?
在大名鼎鼎的stackoverflow上搜索’redis',可以看到排名第一,最高赞的问答就是这个
这已经是2012年的问题了,虽然时过境迁,但这几年我在很多文章中还会看到对这个问题的讨论.
与Memcached的比较对于我们认识redis还是大有裨益的,可以更加了解redis的特色.
在这里就直接把一问一答都贴出来了.
名为Sagiv Ofek的用户需要在Memcached和Redis中做出抉择,他希望了解的是:
哪一个拥有更好的性能?Memcached和Redis彼此都有哪些优点,哪些缺点?比较的范围可以包括:
- 读写的速率
- 内存的使用
- 持久化
- 可拓展性
最高赞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是单线程的,为什么它会这么快?
另一个经典的问题了.
这可以分解成两个问题:
- 为什么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可以帮你做的更好。
- 为什么redis这么快?
Redis有多快可以参考官方的基准测试 : https://redis.io/topics/benchmarks
量级10W ~ 100W QPS.
主要的原因如下:
- 基于内存,所以读写速度都很快.
- 基于单线程,避免了上下文切换和竞态产生的开销.
- 使用epoll实现了I/O多路复用,不会因等待客户端发送数据而造成阻塞.