一个Java码农眼中的技术世界Java高级交流数据库

你对Redis的使用靠谱吗?

2018-03-19  本文已影响2771人  大宽宽
redis

Redis是个流行的in-momery存储。接口好用,性能也很强,还支持多种数据结构,加上各种HA和Cluster方案,实在是居家旅行、杀人灭口、必备良药。


必备良药

但是就是因为太好用了,好用到让很多人都晕了脑子:

这就好像一个股民,在手机上操作买卖几笔股票,赚了一些,然后感叹道"股市就是为我发财而存在的啊"!!

他的下场可想而知。

Redis的种种优势源自于他的设计——简单直接的单线程内存操作。但这些优势是有前提的。

Redis的性能高,吗?

Redis的性能非常高。有些评测说用Redis可以达到几十万QPS(比如这里http://skipperkongen.dk/2013/08/27/how-many-requests-per-second-can-i-get-out-of-redis/)。大家可能在网文上记住了这个NB的数字,却很少关心这个数值怎么来的。这就像是你买手机评测光看跑分一样不靠谱。

Redis要达到高性能需要做到:

所以,如果想真实评价Redis的性能,一定要把你的场景设计好,然后用Redis自带redis-benckmark(见https://redis.io/topics/benchmarks),设定value的尺寸、要测试的Redis命令、和Pipeline的开启情况,再把Redis Server按照生产环境的样子配置好。然后跑一下压测,看看Redis的实际表现到底是怎样的。

Redis可以保证原子性,吗?

我们先定义一下什么是原子性:

很显然,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带来的好处。

用Redis可以实现事务,吗?

我们一般场景下说的事务的意思往往指的是数据库系统中的”ACID事务“。(见https://www.jianshu.com/p/cb97f76a92fd)。ACID事务是计算机科学中一个非常重要的抽象。它极大地简化了编写业务代码的难度。没有ACID事务,开发人员需要花大量精力处理由于并发和系统意外崩溃带来的数据一致性问题。

Redis也有一个“事务”的概念。原文见https://redis.io/topics/transactions。大致含义是:Redis将MULTI指令和EXEC指令之间的多个指令视作一个事务;一旦Redis看到了EXEC就开始执行这一组指令,并保证执行过程中不被打断——除非Redis本身或者所在机器crash掉。如果发生了,就可能出现只有部分指令被执行的情况。

所以,Redis事务与ACID事务是完全不同的

Redis的事务只支持Isolation,不支持ACD。

有人说,AOF的appendfsync=everysec是可以持久化的。但这种持久化只在单机情况下有效。多机情况下,Redis是没有一个机制能够将数据修改同步sync到其他节点的,即便是Redis Cluster的WAIT指令也不行。

在这种限制下,在Redis中实现业务逻辑差不多就只有两种可能:

缓存属于第一个场景。数据丢了没事,从数据库里重新加载就行了。

但如果是第二种场景,你要自己搞一个ACID。不是不可能,但要反复确认这样做的必要性。你是否具有专业的存储开发技能,你能投入多少精力在ACID上,你的公司能给你多少资源做开发测试,这些都需要仔细考虑。

用Redis可以当队列,吗?

Redis实现了一个List的数据结构。借助它,可以实现出队,入队的功能。实际上很多人早就熟练使用Redis做队列。比如Sidekiq就是使用Redis作为异步job队列的存储。然而,这样靠谱吗?

靠谱不靠谱,得看你怎么定义“队列”的要求:

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/streams-intro


Redis适合用来做什么?

在我看来,Redis适合以下场景:

其他场景,往往有更好的、更成熟的方案。

特别注意,不要用Redis存储任何需要“认真对待”的数据,请用支持ACID事务的数据库。

Redis是非常优秀的工具,但非是银弹。只有认真的了解业务对“保证”的要求,认真的了解所用工具的工作原理,才能做出正确的设计决策。


本文来自大宽宽的碎碎念。如果觉得本文有戳到你,请关注/点赞哦。

另外欢迎加入大宽宽的面试经验交流群参加更多讨论。


大宽宽的面试经验交流群
上一篇 下一篇

猜你喜欢

热点阅读