Redis是个流行的in-momery存储。接口好用,性能也很强,还支持多种数据结构,加上各类HA和Cluster方案,实在是居家旅行、杀人灭口、必备良药。redis
必备良药
可是就是由于太好用了,好用到让不少人都晕了脑子:数据库
用Redis性能就大大提升了
用Redis能够保证原子性
用Redis能够实现事务
用Redis能够当队列
……
这就好像一个股民,在手机上操做买卖几笔股票,赚了一些,而后感叹道"股市就是为我发财而存在的啊"!!编程
他的下场可想而知。后端
Redis的种种优点源自于他的设计——简单直接的单线程内存操做。但这些优点是有前提的。缓存
Redis的性能很是高。有些评测说用Redis能够达到几十万QPS(好比这里http://skipperkongen.dk/2013/...)。你们可能在网文上记住了这个NB的数字,却不多关心这个数值怎么来的。这就像是你买手机评测光看跑分同样不靠谱。服务器
Redis要达到高性能须要作到:网络
Value尽量的小。通常的测评都会用比较小的value,好比一个整数或者不长的字符串。可是若是用Redis作缓存,那么缓存的大小的可能偏离这个数字。好比一个页面几十KB;再好比,一个5年的市场价格序列数据可能高达几MB。这么大的数据量在带宽的限制下直接的效果就是QPS骤降,跌到几百都绝不意外。毕竟Redis不是神仙,不能改变物理定律。此外,由于Redis是单线程的,过大尺寸的数据访问会block全部其余的操做。数据结构
使用Pipeline或Lua Script。Redis通常被用作网络服务。全部的请求都是跨网络进行的。因此TCP Round Trip的长短对Redis的性能表现很重要。尽可能减小Round Trip能够有效的提升吞吐。因此,一般的优化方法是使用Pipeline,使得客户端能够一次性把一组Redis命令发给Redis Server;或者预先在Redis Server中定义Lua Script,使用时直接调用。但不管是Pipeline仍是Lua Script,都会受到业务需求的制约——不是全部业务都适合用Pipeline/Lua Script的。并发
使用快一些的网络。不少Redis的测评为了彰显其NB,都是在本地同时跑客户端和服务器的。也就是说,它们要么使用了loopback网络(localhost),要么使用了Unix Socket。这根本就不能反映通常分布式的网络场景下的状况。同时,一些Redis的HA/Sharding方案会选择用Twemproxy这样的代理来实现。代理的加入会让性能进一步的打折扣。app
不开启RDB或者AOF。RDB和AOF是Redis的持久化方案。开启他们会对Redis的性能表现有损耗。好比RDB在开始执行时,会fork一个新的用于写入rdb文件的进程。这个fork的过程和内存空间的复制会让Redis卡顿一下;AOF每次sync数据到磁盘,也会block一小会。若是为了确保数据严格持久化,开启了AOF的appendfsync=everysec设置,使得每一个写入指令都要马上sync到磁盘,就会打破Redis快的前提——内存数据操做。简单来讲,开启任何一种持久化方案都会影响Redis的性能表现。
因此,若是想真实评价Redis的性能,必定要把你的场景设计好,而后用Redis自带redis-benckmark(见https://redis.io/topics/bench...),设定value的尺寸、要测试的Redis命令、和Pipeline的开启状况,再把Redis Server按照生产环境的样子配置好。而后跑一下压测,看看Redis的实际表现究竟是怎样的。
咱们先定义一下什么是原子性:
通常编程语言这么定义:原子性是指一组操做在执行过程当中,不受其余并发操做的干扰。这样进行的数据操做的值不会被相互覆盖。
数据库事务中ACID的A这么定义:原子性是指一组操做,要不完成,要不没作,不存在改了一半的状态。没完成的操做能够回滚。
很显然,Redis并不支持回滚,因此第二条确定没戏。
那么第一条呢?
Redis是单线程执行的。在完成一个操做以前,不会有其余的操做被执行。这的确是真的。可是,在业务开发中,须要的不是一个简单操做的原子性,而须要实现一个临界区的原子性。
业务中对数据的操做每每都不是简单的一个set,一个incr就能够搞定的。一个复杂的业务逻辑,每每须要多个带有逻辑判断的写入指令。业务中要保证的是这一组指令是原子的。好比下面的逻辑,但愿一个value只能越设置越大。
(async function setBiggerV(v) { let currentV = parseInt(await redis.get('key')); if (currentV < v) { await redis.set('key', v); } })();
这实际上是有bug的,考虑到以下执行序列(假设v一开始是5):
client A: 尝试将v设置为7 client B:尝试将v设置为8
读取key,获得5
读取key,获得5
设置key,为8
设置key,为7
最终,Redis中v的值被设置为7,这就违反了这段逻辑的设计。若是这个机制被应用于协调一个分布式系统,那么整个系统就会所以挂掉。set这个命令是否是原子并不能让这段业务代码变成原子的。咱们须要的是让get和set这个总体原子。
在Redis中,能够用Redis事务或者Lua Script来实现原子性。Redis事务和Lua Script均可以保证一组指令执行不受其余指令的打扰。好比上面的例子,用Lua Script实现,就能够正确运行。
但若是业务逻辑涉及到其余存储,Redis事务和Lua Script就帮不上忙。好比,在Redis中放一个库存的数字。用户下单时,要在Redis中扣减库存,而且在另一个数据库中INSERT一条交易记录。这段逻辑是无法作到原子的——除非你自行实现了某种分布式事务的机制。而分布式事务的实现复杂度每每会超过Redis带来的好处。
咱们通常场景下说的事务的意思每每指的是数据库系统中的”ACID事务“。(见https://www.jianshu.com/p/cb9...)。ACID事务是计算机科学中一个很是重要的抽象。它极大地简化了编写业务代码的难度。没有ACID事务,开发人员须要花大量精力处理因为并发和系统意外崩溃带来的数据一致性问题。
Redis也有一个“事务”的概念。原文见https://redis.io/topics/trans...。大体含义是:Redis将MULTI指令和EXEC指令之间的多个指令视做一个事务;一旦Redis看到了EXEC就开始执行这一组指令,并保证执行过程当中不被打断——除非Redis自己或者所在机器crash掉。若是发生了,就可能出现只有部分指令被执行的状况。
因此,Redis事务与ACID事务是彻底不一样的!
Redis的事务只支持Isolation,不支持ACD。
有人说,AOF的appendfsync=everysec是能够持久化的。但这种持久化只在单机状况下有效。多机状况下,Redis是没有一个机制可以将数据修改同步sync到其余节点的,即使是Redis Cluster的WAIT指令也不行。
在这种限制下,在Redis中实现业务逻辑差很少就只有两种可能:
不在乎ACID事务——数据丢了没事,改错了也没大关系
基于Redis的接口实现本身的ACID,或者ACID的某种子集
缓存属于第一个场景。数据丢了没事,从数据库里从新加载就好了。
但若是是第二种场景,你要本身搞一个ACID。不是不可能,但要反复确认这样作的必要性。你是否具备专业的存储开发技能,你能投入多少精力在ACID上,你的公司能给你多少资源作开发测试,这些都须要仔细考虑。
Redis实现了一个List的数据结构。借助它,能够实现出队,入队的功能。实际上不少人早就熟练使用Redis作队列。好比Sidekiq就是使用Redis做为异步job队列的存储。然而,这样靠谱吗?
靠谱不靠谱,得看你怎么定义“队列”的要求:
队列可不可能丢东西?好比,若是队列短期挂掉。此时,producer是必须中止服务,仍是继续服务但再也不插入队列(这样就会丢东西),或者说producer有某种机制能够在本地先暂时堆积一下,直到队列恢复工做?
队列的consumer是否须要一个“commit”的语义,表示处理完了一个事件?仍是说,只要从队列里取出来就能够了,万一没处理也没所谓?
是否有事件重放的须要?好比上线了一个版本的consumer而后发现有bug,处理错了3个小时的数据。修复后,但愿能从新处理一遍以前出错的数据,那么这个队列能不能作事件的”重放“?
若是consumer处理失败怎么处理?是直接丢弃,仍是从新插入到队列中?
队列是否是须要有最大的长度限制?若是到了最大长度,说明Consumer跟不上Producer的速度;此时,须要卡住Producer吗?
……
Redis的List基本上对于全部这些问题都是彻底无论的。也就是说,它不能给你任何的保证。更严重的是,就算你能接受必定程度的数据丢失,可是Redis没法告诉你他丢了多少东西,而且找不回来(MySQL还能翻翻binlog)!到最后,到底丢了多少,形成多少损失,是没法监控,是没法衡量的。
在业务上,“保证”一个事情可以发生至关重要。试想一下,你的界面容许用户下一笔订单,用户已经看到了“成功下单”的界面,结果以后却发现什么订单也没有。用户是否是有一句MMP不知道当讲不当讲。
也许,你会说,"个人场景不须要这么严格的一致性,数据丢了没所谓,也不须要事件重放,数据处理错了就错了"。这个Redis的确能够办到,并且能够作得很好。但我建议你和你的产品经理聊一下,看看需求是否是真的这样。也许他会有不一样的意见 ; - )
通常来说,一个技术公司须要两大类“队列”。一种是业务事件队列。这种队列绝对不能丢东西,并且可能须要exactly once语义,须要高可用。为了保证可用性,多节点的部署是必须的。而引入了多节点,就必须解决复制的问题和分布式一致的问题,主从切换的问题,分片的问题等。这种队列的典型表明是Rabbit MQ和Kafka。
另一种队列是收集服务先后端业务事件的队列(好比登录、注册、下单成功、下单失败……)。经过队列,这些事件会被收集到数据分析中心,支持错误分析、客服、数据分析等功能。这种队列能够容忍一些数据丢失,也能容忍数据延迟性比较大,但要求吞吐巨大。这种队列的典型表明是Fluentd和Logstash。
也许你一开始在用Redis的List作队列,可是若是这个业务是认真的,你的系统必定会逐渐演进到这两者之一。
Redis 4.2计划引入Disque做为新的队列实现。也许可以扭转这个状况。但4.2离发布还要好久,而且成熟到能够在生产使用,也至少要到4.4版本——大概在2019年甚至更晚。因此目前观望一下就好,没必要特别在乎。
更新一下:Redis 5.0beta引入了Stream Date Type。实现了相似于Kafka的append only数据结构和API。不过极可能要到5.2才能在生产中使用(2019年年末)。见https://redis.io/topics/strea...
Redis适合用来作什么?
其余场景,每每有更好的、更成熟的方案。
特别注意,不要用Redis存储任何须要“认真对待”的数据,请用支持ACID事务的数据库。
Redis是很是优秀的工具,但非是银弹。只有认真的了解业务对“保证”的要求,认真的了解所用工具的工做原理,才能作出正确的设计决策。
做者:大宽宽
连接:https://www.jianshu.com/p/9ce...
來源:简书
简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
---------------------------------Z