详情见下图,缓存不会主动加载数据,而是根据Web请求懒加载数据。对于请求k1数据来说,发现缓存没有对应数据,到DB查询,然后放入Cache,这是常规流程;但如果有突发流量,大量请求同时访问k2数据,但Cache中没有数据时,请求就会同时落到DB上,可能压垮数据库。
缓存过期策略
依赖时间的过期策略
定时删除
对于需要删除的每个Key都配备一个定时器,元素超时时间一到就删除元素,释放元素占用的内存,同时释放定时器自身资源。其优点是元素的删除很及时,但缺点也很明显,比如为每个Key配备定时器肯定会消耗CPU和内存资源,严重影响性能。这种策略只适合在小数据量且对过期时间又严格要求的场景能使用,一般生产环境都不会使用。
惰性删除
元素过期后并不会立马删除,而是等到该元素的下一次操作(如:访问、更新等)才会判断是否过期,执行过期删除操作。这样的好处是节约CPU资源,因为只有当元素真的过期了,才会将其删除,而不用单独管理元素的生命周期。但其对内存不友好,因为如果若干已经过期的元素一直不被访问的话,那就会一直占用内存,造成内存泄漏。
定期删除
以上两种元素删除策略各有优缺点,无非是对CPU友好,还是对内存友好。为了结合两者的优点,一方面减少了元素定时器的配备,只使用一个定时器来统一扫描过期元素;另一方面加速了判断元素过期的时间间隔,不是被动等待检测过期,而是间隔一段时间就主动执行元素过期检测任务。正是由于以上的改进点,此方案是元素过期检测的惯常手段。
我们假设一个场景,为了保护用户隐私,通常在用户电话和商家电话之间,会使用一个虚拟电话作为沟通的桥梁。业务使用中,往往同一个虚拟号码在一定时间内是可以对相同的用户和商家建立连接的,而当超出这个时间后,这个虚拟号码就不再维护映射关系了。
虚拟电话号码的资源是有限的,自然会想到创建一个虚拟号码资源池,管理虚拟号码的创建和释放。比如规定一个虚拟号码维持的关系每次能使用15分钟,那么过期后要释放虚拟号码,我们有什么方案呢?
A. 方案一:全量数据扫描,依次遍历判断过期时间
对于DB中存储的以上内容,每天记录都存储着虚拟号码的创建时间,以及经过expire_seconds就会删除此记录。那么需要配备一个定时任务扫描表中的所有记录,再判断current_time - create_time >expire_seconds,才会删除记录。
如果数据量很大的情况,就会导致数据删除延迟时间很长,这并不是可取的方案。那是否有方案能直接获取到需要过期的vr_phone,然后批量过期来解决上述痛点呢?来看看方案二吧。
B. 方案二:存储绝对过期时间+BTree索引,批量获取过期的vr_phone列表
将相对过期时间expire_seconds改为记录过期的时间戳expire_timestamp,同时将其添加BTree索引提高检索效率。仍然使用一个定时器,在获取待删除vr_phone列表时只需要select vr_phone from table where now()>=expire_timestamp即可。
对于空间复杂度增加了一个BTree数据结构,而基于BTree来考虑时间复杂度的话,对于元素的新增、修改、删除、查询的平均时间复杂度都是O(logN)。
此方案已经能满足业务使用需求了,那是否还有性能更好的方案呢?
d) 单层定时轮算法
我们继续讨论上面的案例,寻找更优的解题思路。下表是DB存储元素:
此时DB中不再存储和过期时间相关的数据,而专注于业务数据本身。对于过期的功能我们交给单层定时轮来解决。其本质是一个环形数组,数组每一格代表1秒,每次新加入的元素放在游标的上一格,而游标所指向的位置就是需要过期的vr_phone列表。
执行过程:
1、初始化:启动一个timer,每隔1s,在上述环形队列中移动一格,1->2->3...->29->750->1...有一个指针来标识有待过期的slot数据