当前位置: 首页 > 科技观察

Redis只能做缓存?太过分了!

时间:2023-03-12 04:16:40 科技观察

本文转载自微信公众号“小姐姐的味道”,作者小姐姐养的狗。转载本文请联系味觉小姐公众号。大多数数据库,因为经常和磁盘打交道,所以在高并发场景下响应非常慢。为了解决这种速度差异,大部分系统习惯性地加了一个缓存层来加速数据读取。由于其出色的处理能力和丰富的数据结构,redis已经成为事实上的分布式缓存标准。但是,如果你认为redis只能用来做缓存,那你就小看它了。redis丰富的数据结构使得它的业务使用场景非常广泛,再加上rdb的持久化特性,甚至可以作为登陆数据库使用。在这种情况下,redis可以支持大部分互联网公司的一半,尤其是社交、游戏、直播公司。1、Redis可以进行存储工作。Redis提供了非常丰富的集群模式:master-slave、sentinel、cluster,以满足服务高可用的需求。同时redis提供了两种持久化方式:aof和rdb,常用的是rdb。通过bgsave命令,主进程会fork一个新的进程并写回磁盘。bgsave相当于拍一张快照。由于没有WAL日志和checkpoint机制,所以无法实时备份。如果机器突然断电,很容易丢失数据。好在redis是内存数据库,主集群同步速度很快。如果你的集群维护得好,内存分配合理,那么除非机房断电,否则redis的SLA会一直保持在很高的水平。听起来不是绝对可靠,有数据丢失的可能!这在一般的CRUD业务中是难以忍受的。但是redis为什么能够满足大部分互联网公司的需求呢?这也是由业务属性决定的。在决定最大限度地拥抱Redis之前,需要先确认自己的业务是否具备以下特点:除了核心业务之外,大部分业务对数据可靠性的要求是否较低,丢失一两条数据是可以容忍的?C端用户可以根据用户ID快速定位到一类数据。数据集普遍很小?不需要大量的范围查询?基于内存的数据成本是否可以承受?业务是否几乎不需要事务操作?幸运的是,这类业务有很多特殊需求。比如常见的社交服务、游戏、直播、运营服务都可以完全依赖Redis。2.Reids应用场景Redis具有松散的文档结构,丰富的数据类型,能够适应千变万化的方案变化。接下来介绍Redis除了缓存以外的大量应用场景。2.1传统数据库设计存储用户基础数据,用户表设计难度很大,改了会伤筋动骨。利用Redis的哈希结构,可以实现松散的数据模型设计。某些不固定的、验证类的功能属性可以通过JSON接口直接存储在hash的值中。使用hash结构,可以使用HGET、HMGET等命令只获取自己需要的数据,使用起来也很方便。>HSETuser:199929sexm>HSETuser:199929age22>HGETALLuser:1999291)"sex"2)"m"3)"age"4)"22"这种读多写少的非统计场景很适合用KV结构被存储。Redis的hash结构提供了非常丰富的指令,某个属性也可以使用HINCRBY自增自减,非常方便。2.2计数器的实现上面稍微提到了HINCRBY指令,对于RedisKey本身,还有INCRBY指令来实现某个值的自增自减。比如下面的场景:统计一个帖子的点赞数;存储某个主题的关注者数量;存储某个标签的粉丝数;存储评论的一般数量;某个帖子的受欢迎程度;红点消息的数量;点赞数、点赞数等>INCRBYfeed:e3kk38j4kl:like1>INCRBYfeed:e3kk38j4kl:like1>GETfeed:e3kk38j4kl:like"2"对于像微博这样容易出现热点的商家,传统数据库肯定做不到支持它,所以必须依赖内存数据库。因为Redis非常快,不需要使用传统DB那种非常慢的计数操作。所有这些增量操作都是毫秒级的,效果是实时的。2.3LeaderboardsLeaderboards可以提高参与者的积极性,所以这个业务很普遍,本质上是一个topn问题。Redis中有一个数据结构叫zset。使用跳表实现的有序列表,很容易实现排行榜等问题。当zset存储的数据达到千万级甚至亿级的时候,依然可以保持非常高的并发读写,并且有非常好的平均响应时间(5ms以内)。使用zadd添加一条新记录,我们将排名相关的分数作为记录的分值,然后使用zrevrange命令获取实时排行榜数据,zrevrank可以很方便地获取用户的实时排名容易地。>ZADDsorted:xjjdog:2021-0755dog0>ZADDsorted:xjjdog:2021-0789dog1>ZADDsorted:xjjdog:2021-0732dog2>ZCARDsorted:xjjdog:2021-07>3>ZREVRANGEsorted:xjjdog:2021-070SCORES)"dog1"2)"89"3)"dog0"4)"55"5)"dog2"6)"32"2.4好友关系集合结构是一个没有重复数据的集合,可以设置一个用户的关注列表、粉丝列表、双向关注列表、黑名单,如列表等,存储在单独的zsets中。使用ZADD、ZRANK等使用ZADD添加用户的黑名单,ZRANK根据返回的sorce值判断是否存在于黑名单中。使用sinter命令可以获取A和B的共同好友,除了好友关系,还可以使用set结构存储黑名单和白名单业务场景明确的数据。这样的业务场景有很多,比如用户上传的通讯录,计算通讯录的好友关系等等。在实际使用中,多使用zset来存储这种关系。和set一样,zset不允许重复值,但是zset多了一个score字段,我们可以存储一个时间戳来表示关系建立的时间,这样有更明确的业务意义。2.5统计活跃用户数类似于统计日活跃用户、用户登录、用户在线状态。零散的需求太多了。如果为每个用户存储一个bool变量,它会占用太多空间。在这种情况下,我们可以使用位图结构来节省大量的存储空间。>SETBITonline:2021-07-2338765203331>SETBITonline:2021-07-2438765203331>GETBITonline:2021-07-2338765203331>BITOPANDactiveonline:2021-07-23online:2021-07-24>GETBITactive38765203331>DEBUGOBJECTonline:2021-07-23Valueat:0x7fdfde438bf0refcount:1encoding:rawserializedlength:5506446lru:16410558lru_seconds_idle:5(0.96s)注意,如果你的id很大,需要先进行预处理,否则会占用大量内存。位图包含一系列连续的二进制数,用1位来表示真假问题。在位图上,可以使用与、或、异或等位运算(bitop)。2.6分布式锁Redis的分布式锁是一种轻量级的解决方案。虽然其可靠性不如Zookeeper等系统,但Redis分布式锁具有极高的吞吐量。最简单的锁定操作之一可以通过使用带有nx和px参数的redisset命令来完成。下面是一小段简单的分布式示例代码。publicStringlock(Stringkey,inttimeOutSecond){for(;;){Stringstamp=String.valueOf(System.nanoTime());booleanexist=redisTemplate.opsForValue().setIfAbsent(key,stamp,timeOutSecond,TimeUnit.SECONDS);if(存在){returnstamp;}}}publicvoidunlock(Stringkey,Stringstamp){redisTemplate.execute(script,Arrays.asList(key),stamp);}删除操作的lua是。localstamp=ARGV[1]localkey=KEYS[1]localcurrent=redis.call("GET",key)ifstamp==currentthenredis.call("DEL",key)return"OK"endredisson的RedLock是最常用的分布式lock方案具有读写锁的区别,处理多个redis实例情况下的异常问题。2.7分布式限流在Redis中使用计数器实现简单的限流是非常方便的。只需要使用incr配合expire命令即可。incrkeyexpirekey1的简单实现通常没有问题,但是在流量比较大的情况下,存在时间跨度突然流量暴增的风险。根本原因在于这种时间分片方式过于固定,没有像滑动窗口那样平滑过渡的方案。同样是redisson的RRateLimiter,实现了类似guava中的分布式限流工具类,使用起来非常方便。下面是一个简短的例子:RRateLimiterlimiter=redisson.getRateLimiter("xjjdogLimiter");//只需要初始化一次//5permitsevery2secondslimiter.trySetRate(RateType.OVERALL,5,2,RateIntervalUnit.SECONDS);//没有license可用,会一直blocklimiter.acquire(3);2.8消息队列Redis可以实现一个简单的队列。在生产者端,使用LPUSH将其添加到某个列表;消费端使用RPOP命令连续取数据,或者使用阻塞式BRPOP命令取数据,适合小规模抢购需求。Redis也有PUB/SUB模式,但是pubsub更适合消息广播等业务。在Redis5.0中,增加了流类型的数据结构。它类似于Kafka,有topic和consumergroup的概念,可以实现组播和持久化,已经可以满足大部分业务需求。2..9LBS应用早在Redis3.2版本就推出了GEO功能。通过GEOADD命令添加lat和lng经纬度数据,可以实现坐标间距离计算、包含关系计算、附近人等功能。关于GEO功能,最强大的开源解决方案是基于PostgreSQL的PostGIS,但对于一般规模的GEO服务,redis就足够了。2.10更多扩展的应用场景就看redis能干什么了,不得不提下面的java客户端库redisson。Redisson包含丰富的分布式数据结构,所有这些都是基于redis设计的。Redisson提供了Set、SetMultimap、ScoredSortedSet、SortedSet、Map、ConcurrentMap、List、ListMultimap、Queue、BlockingQueue等多种数据结构,使得基于redis的编程更加方便。在github上,你可以看到有数百种这样的数据结构:https://github.com/redisson/redisson/tree/master/redisson/src/main/java/org/redisson/api。对于某种语言,数组、链表、集合等基本API可以一起完成大部分业务开发。Redis也不例外。它具备这些基本的API操作能力,也可以组合成分布式、线程安全、高并发的应用。由于Redis是基于内存的,所以它的速度非常快,我们也会将它作为中间数据的存储场所。比如一些公共配置共享到redis中,它就充当配置中心;例如,将JWTtoken存储在Redis中,可以突破JWT的一些限制,实现安全登出。3、一站式Redis面临的挑战Redis拥有丰富的数据结构,一般不会在功能上造成困扰。但是随着请求量的增加,SLA要求的提高,我们势必会对Redis进行一些改造和定制化开发。3.1高可用挑战Redis提供三种集群模式:master-slave、sentinel和cluster,其中集群模式目前被大多数公司采用。但是redis的集群模式有很多缺陷。Redis集群利用虚拟槽的概念,将所有的key映射到0到16383的整数槽中,属于去中心化架构。但其维护成本高,slave不能参与读操作。它的主要问题在于一些批处理操作的局限性。由于key散列到多台机器上,mget、hmset、sunion等操作非常不友好,经常会出现性能问题。redis的主从模式是最简单的模式,但是无法实现自动故障转移。通常,主从切换后,需要修改业务代码,这是不能容忍的。即使有haproxy等负载均衡组件,复杂度也非常高。Sentinel模式在master和slave数量较多的情况下更能显着体现其价值。一个Sentinel集群可以监控成百上千个集群,但是Sentinel集群本身的维护难度更大。幸运的是,redis的文本协议非常简单。netty中甚至直接提供了redis的codec。自行开发一个哨兵系统,并加强其功能是可行的。3.2冷热数据分离Redis的特点是无论是何种数据,都存储在内存中进行计算。这对有时间序列概念和冷热数据的业务提出了非常高的成本考验。为什么大多数开发者喜欢将数据存储在MySQL而不是Redis中?除了交易需求,很大的一个原因就是历史数据的问题。通常,这种冷热数据的切换是由中间件完成的。上面我们说了,Redis是一个文本协议,非常简单。做一个中间件,或者协议兼容的Redis模拟存储,还是比较容易的。比如我们的Redis中,只保留上一年的活跃用户。一个多年不活跃的用户突然访问系统。这时候我们获取数据的时候,就需要中间件进行转换,从容量大、速度慢的存储中查找。这时候Redis的作用更像是一个热库,更像是一个传统的缓存层做的事情,业务上规模之后才会出现。但是注意,直到现在,我们的业务层代码一直都是操作的redisapi。他们使用这些无数的函数指令,并不关心数据实际上是存储在redis中还是存储在ssdb中。3.3功能需求redis可以玩很多花样。例如,全文搜索。很多人会更喜欢es,但是redis生态提供了一个模块:RediSearch,可以做查询和过滤。但是我们通常会有更多的需求,比如统计、搜索、运营效果分析等。这种需求跟大数据有关,传统的DB都做不到。这时候当然需要将redis中的数据导入到其他平台进行计算。如果选择redis数据库,那么dba处理的是rdb,而不是binlog。rdb解析工具有很多(如redis-rdb-tools),可以定时将rdb解析成记录,并导入到其他平台,如hadoop。至此,rdb成为所有团队的中坚力量,成为基本的数据交换格式。业务导入其他db后,怎么玩怎么玩,不会因为业务系统用redis而无法运行。4.总结大部分业务系统运行在redis上,这对于很多一直使用MySQL作为业务系统的同学来说是难以想象的。看完上面的介绍,相信大家对redis可以实现的存储功能已经有了一个大概的了解。打开你的社交APP、游戏APP、视频APP,看看它们的功能能覆盖到多少?我想在这里强调的是,某些数据不一定非要存储在RDBMS中才被认为是安全的。它们不是强烈的需求。既然redis这么强大,为什么还需要mysql、tidb这样的存储呢?关键在于业务属性。如果一个业务系统每次与数据进行交互,都是一个非常庞大的结果集,涉及到非常复杂的统计和过滤工作,那么RDBMS就必不可少;但是如果一个系统能够快速的定位到一类数据的时候,这种数据在可预见的将来是有限的,所以非常适合用Redis存储。对于电商系统来说,选择redis做存储是要命的,而社交系统就快乐多了。为合适的场景选择合适的工具,才是我们应该做的。但是,一个系统能否在产品验证期快速响应变化,快速开发上线,是成败的关键。这也是使用redis作为数据库所能带来的最大好处。不要被极低概率的数据丢失情况吓倒。你的系统,即使它像钢铁一样坚固,与你产品的成功相比也一文不值。品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。