首先为什么要做一个redis出来?数据库不够用了吗?考虑到原本的应用程序是客户端访问服务端,服务端访问业务数据需要去数据库去拿,而数据库是个持久化的应用程序,是需要磁盘IO的,这就导致了速度会慢,并且如果存在大量的访问,会导致数据库崩溃。除去导致崩溃这样严重且极端的情况,这点性能虽然对一个二个用户来说并没有什么感知,但是对于应用程序来说,是个有着很大加速空间的点,为了吃这部分性能,出现了redis[1],承担了缓存的作用。目前,redis因为它的高性能,出现了很多应用场景。
什么是Redis?为什么要用Redis? - 知乎 (zhihu.com)
做一个缓存简单,应用程序选择合适的数据结构向redis中存储键值对,redis开辟内存空间存储,一个缓存就做好了。有什么问题吗?想想应用程序是如何使用redis的:不论是数据库亦是应用程序本身的键,选择redis提供的数据结构,然后向里面set后,数据就保存在里面了,就这么一直一直放,即便有过期时间,也有把内存塞满的可能,哪怕几率很小,那作为一个为其他服务提供服务的服务,也要解决这个问题——不能让应用程序把自己的内存空间塞满而崩溃。
这样就会主要涉及到两个问题:
- 过了期的键怎么删除
- 内存满了怎么处理
针对上面的两个问题,按顺序介绍解决方案,以及为什么是这样:
-
删除过期键?
挨个检索是不现实的,数量太大,如果停下来去专门清理,则会影响到主要的业务,那么就需要其他的策略。但是删除前需要准确地找到设置了过期时间的键,这涉及到redis的内存是怎么组织的(键管理)。
(这个图我不会画,也画不好,因此找到了网上几篇优秀的博客的图片并附上链接以作补充)
图片 来源 一文讲清楚Redis 内存模型,Redis 的快是有理由的! - 知乎 (zhihu.com) 详解 Redis 内存管理机制和实现 - 知乎 (zhihu.com) 左图是一对键"hello"->"world"的存储模型,如果是默认的存储(默认redis创建16个db,redis-cli连接到redis-server使用的是db0),并且没有设置过期时间,就是存到db0的redisDb中的dict下,table指向dictEntry按顺序往下存储,dictEntry的组织形式如图所示。内存模型传递的一个信息很重要,也就是设置了过期时间的键存储在一起的,这也是redis可以随机删除过期了的键的基础,不然删除时找一个键是永久的,再找一个还是永久的,删除过期的键这个任务又要快速,岂不是这次任务执行的效率极低?
从内存模型扯回来谈过期策略。redis提供了两种过期策略,分别是:
- 惰性删除: 惰性删除很好理解,当有人要用到这个键,查到键是在过期字典里,那先判断是不是过期了,如果过期了就不返回,并且将键删除。可能有人有疑问,既然还在内存中,已经有请求想要访问这个键,为什么不返回一次再删除?我个人的理解是绝对不可以返回,有几点理由:
- 返回不符合逻辑:已经过期的键,理想状态下就应该不存在。我是被动地让它存在在内存中,如果我有更好的办法能让它自动消失,它早已不存在,返回的话语义不一致。
- 可能造成错误:这个要根据场景来具体地考虑。比如存储token,如果token已经过期,那么用户携带的token不应该能通过登录,如果还能成功返回用户就可能登录成功访问账户里的资源,这一操作可能是危险的。(比如token过期了几天,这个键值对都好运地没有被删除)
- 定时删除: 这是一个相对复杂的算法。现在我们已知的是,所有存放了过期时间的键已经码好了,排成一排放在那,redis提供的策略是每秒扫描 10 次,即100ms检查一次是否有键过期,这是可以通过配置调整的。不过这一过程不能扫描全部,上面也提到这样做会阻塞线程,导致主要的工作长时间不响应,本身redis就是用来应对高并发,这是致命的。那么具体是怎么做的呢?首先推荐一篇博客,从源码级别来解读,并且条理清晰:Redis学习笔记(三)redis 的键管理 - 归思君 - 博客园 (cnblogs.com)不过,既然作为我自己的学习笔记,一定要留下我的理解和记录,大致流程——从过期字典中随机抽取20个键进行删除,如果删除超过25%就再次执行,再抽取20个键删除,直到删除的个数小于等于25%,这是do-while循环中做的。并且会确定一个执行删除算法的时长,如果执行删除算法的时间太长,则会退出这个删除过程,默认删除过程最多执行25ms左右。也即如果这时有key想要set,最多也就等待25ms。redis删除的操作显得格外小心,以提供安稳的服务,这对于我们应用开发者有启示吗?当然是有的。这提示我们要设置合理的过期时间,在应用程序中也要精打细算,尽可能不然大量的key在同一时间过期[2],那么最佳实践是如果有很多key要一起放入redis中的时候,最好将过期时间分散一下,在”应用需要的过期时间后“加上一个随机的散列值,使得删除时的循环尽可能地早退出。
- 惰性删除: 惰性删除很好理解,当有人要用到这个键,查到键是在过期字典里,那先判断是不是过期了,如果过期了就不返回,并且将键删除。可能有人有疑问,既然还在内存中,已经有请求想要访问这个键,为什么不返回一次再删除?我个人的理解是绝对不可以返回,有几点理由:
-
内存(快)满了怎么办?
即便有过期时间的限制,redis依然有可能被“挤满”,这是不可避免的。而redis无法控制应用程序的行为,那么只能本身提供坚强的服务,当内存满了的时候,redis应该怎么做能让自己运行的更久,这是redis的开发者应该考虑的事情。
没有什么别的办法,内存满了就只能删除,即常常提到的内存淘汰。首先是时机,redis有很多监控自己状态的指标,这里用到的是maxmemory。字面意思,当使用的内存达到了这个值,那么redis就认为自己已经存满了,就要执行一些内存淘汰策略来释放自己的空间。主要有以下几种策略:
- noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,默认。
- allkeys-lru:淘汰整个键值中最久未使用的键值。
- allkeys-random:随机淘汰任意键值。
- volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值。
- volatile-random:随机淘汰设置了过期时间的任意键值。
- volatile-ttl:优先淘汰更早过期的键值。
- **volatile-lfu: **淘汰所有设置了过期时间的键值中最少使用的键值。
- **alkeys-lfu: **淘汰整个键值中最少使用的键值
alkeys 开头的表示从所有键值中淘汰相关数据,而 volatile 表示从设置了过期键的键值中淘汰数据。