Redis缓存策略以及bigkey的学习(九)
一、Redis内存
1.1、通过redis.conf配置文件查看内存
1.2、通过命令查看内存
config get maxmemory
为0?那这些key存哪去了?
原来不设置最大内存大小或者设置内存大小为0,在64位系统下不限制内存大小,在32位系统下最多使用大约3GB的内存。
1.3、一般设置内存大小多少合适?
一般设置为物理内存的四分之三左右。
1.4、如何设置redis内存大小?
-
- 修改配置文件,重启redis服务。
maxmemory 1024 # 注意单位是bytes
-
- 通过命令动态设置内存大小。
config set maxmemory 1024
1.5、查看内存使用情况命令
除了之前的config get maxmemory命令,还可以通过info命令查看内存使用情况。
info memory
1.6、如果Redis内存超出最大内存大小会发生什么?
会报OOM错误。
小结: 如果key不加上过期时间,随着时间推移,内存会越来越大,然后写满maxmemory大小,就会报OOM错误;为避免这个错误,需要了解内存淘汰策略
二、内存淘汰策略
2.1、redis过期键是如何删除的?
-
- 立即删除策略:保证过期键在过期后马上被删除,所占用的内存立即被释放,但对CPU不友好,需要时刻遍历设置生存时间的key,会产生大量的CPU消耗,影响性能。
- 立即删除策略:保证过期键在过期后马上被删除,所占用的内存立即被释放,但对CPU不友好,需要时刻遍历设置生存时间的key,会产生大量的CPU消耗,影响性能。
-
- 惰性删除策略:数据达到过期时间,不做处理,只有在访问过期键时才会检查其是否过期,如果过期则删除。这种方式对CPU友好,但内存占用时间长,可能导致大量过期key没有被及时清理,造成内存溢出。
支持惰性删除,需要在redis.conf配置文件中开启:lazyfree-lazy-eviction yes
- 惰性删除策略:数据达到过期时间,不做处理,只有在访问过期键时才会检查其是否过期,如果过期则删除。这种方式对CPU友好,但内存占用时间长,可能导致大量过期key没有被及时清理,造成内存溢出。
-
- 定时删除策略(折中,推荐):每隔一段时间,遍历设置生存时间的key,随机检查是否有过期键,如果有则删除。这种方式介于前两种方式之间,既保证了内存及时释放,也减少了CPU消耗。
这种方式难点在于:把握好删除时长和频率,不然容易退化成为上面两种策略的极端情况。
小结:
这三种方案还是存在一些问题,比如定期删除时,有些key从没有被访问过,导致没有被删除;惰性删除时,也有一些key从没有被访问过,导致没有被删除,就会使得redis内存空间紧张。
- 定时删除策略(折中,推荐):每隔一段时间,遍历设置生存时间的key,随机检查是否有过期键,如果有则删除。这种方式介于前两种方式之间,既保证了内存及时释放,也减少了CPU消耗。
2.2、redis缓存策略
八大策略:
- noeviction:不删除任何key,即使内存达到上限也不进行删除,此时增加key会返回error。
- allkeys-lru:对所有key使用LRU算法淘汰key。
- volatile-lru:对设置了过期时间的key使用LRU算法淘汰。
- allkeys-lfu: 对所有key使用LFU算法淘汰。
- volatile-lfu: 对设置了过期时间的key使用LFU算法淘汰。
- allkeys-random:对所有key随机淘汰。
- volatile-random:对设置了过期时间的key随机淘汰。
- volatile-ttl:对设置了过期时间的key,根据过期时间立即淘汰
2.3、LRU和LFU的区别
- LRU(Least Recently Used):最近 最少使用 ,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。
- LFU(Least Frequently Used):最近 最不常使用 ,淘汰一定时期内访问次数最少的页面,看一定时间段内页面被访问的频率高低,淘汰一定时期内被访问次数最少的页面。
特性 | LRU | LFU |
---|---|---|
淘汰依据 | 淘汰最长时间未被使用的页面 | 淘汰一定时间内访问次数最少的页面 |
数据结构 | 双向链表 + 哈希表 | 双哈希表 + 频率链表 |
时间复杂度 | O(1) | O(1) |
/** LRU缓存实现 **/
template<typename K, typename V>
class LRUCache
{public:LRUCache(size_t capacity) : m_capacity(capacity){}V get(K key){auto it = m_cache.find(key);if (it == m_cache.end()) throw out_of_range("Key not found");m_lru.splice(m_lru.begin(), m_lru, it->second); // 将节点移动到链表头部return it->second->second;}void put(K key, V value){auto it = m_cache.find(key);if (it != m_cache.end()){it->second->second = value; // 更新值m_lru.splice(m_lru.begin(), m_lru, it->second); // 更新节点位置return;}// 检查容量是否超出,如果超出则删除最旧的元素(尾部)if (m_lru.size() >= m_capacity){// 淘汰尾部元素auto last = m_lru.back();m_cache.erase(last->first); // 从哈希表中删除该元素m_lru.pop_back(); // 移除链表中的最后一个节点}// 插入新元素到头部m_lru.emplace_front(key, value);m_cache[key] = m_lru.begin();}void print(){for (auto& item : m_lru)cout << "Key: " << item.first << ", Value: " << item.second << "-> ";cout<<"nullptr\n";}private:size_t m_capacity;list<pair<K, V>> m_lru; //存储键值对,头部最新,尾部最旧unordered_map<K, typename list<pair<K, V>>::iterator> m_cache;
};
/** LFU缓存实现 **/
template <typename K, typename V>
class LFUCache
{
public:struct Node {K key;V value;int freq; // 频率Node(K key, V val, int f) :key(key), value(val), freq(f) {}};LFUCache(size_t capacity) :m_capacity(capacity), m_minFreq(0) {}V get(K key){if (m_cache.find(key) == m_cache.end()) throw out_of_range("Key not found");// 获取节点,并更新频率auto node = m_cache[key];updateFreq(node);// 重新使迭代器指向keynode = m_cache[key];return node->value;}void put(K key, V value){if (m_capacity <= 0) return;// 如果键已存在, 更新值和频率if (m_cache.find(key) != m_cache.end()) {auto node = m_cache[key];node->value = value;updateFreq(node);return;}// 如果缓存已满,则移除最少使用的节点if (m_cache.size() >= m_capacity){// 移除最少使用的节点auto& minFreqList = m_freqList[m_minFreq];auto del_node = minFreqList.back();m_cache.erase(del_node.key);minFreqList.pop_back();// 如果链表为空,清除频率if (minFreqList.empty()) {m_freqList.erase(m_minFreq);if (m_minFreq > 1) --m_minFreq; // 更新最小频率值}}// 插入新节点,频率为1m_minFreq = 1; // 新节点频率为1,更新最小频率值m_freqList[m_minFreq].emplace_front(key, value, m_minFreq);m_cache[key] = m_freqList[1].begin();}void print(){cout << "==============================\n";cout << "Print\n";for (auto& val : m_freqList) {cout << "Frequency: " << val.first;for (auto& node : val.second) {cout << " Key: " << node.key << " value: " << node.value << endl;}}cout << "==============================\n";}
private:size_t m_capacity;int m_minFreq; // 记录当前最小的频率unordered_map<K, typename list<Node>::iterator> m_cache;unordered_map<int, list<Node>> m_freqList; // 频率列表,key为频率值,value为该频率下所有节点构成的链表void updateFreq(typename list<Node>::iterator node){// 保存节点的键和值K key = node->key;V value = node->value;// 从原频率链表移除int old_freq = node->freq;auto& old_list = m_freqList[old_freq];old_list.erase(node);// 如果频率链表为空,则更新最小频率值if (old_list.empty()) {m_freqList.erase(old_freq); // 移除空链表if (old_freq == m_minFreq)++m_minFreq;}// 插入到新频率链表中int new_freq = old_freq + 1;m_freqList[new_freq].emplace_front(key, value, new_freq);m_cache[key] = m_freqList[new_freq].begin();}
};
2.4、使用哪种策略好?
根据使用场景来定:
-
- 在所有的key都是最近经常使用,那么就需要选择allkeys-lru策略,替换掉最近最不常使用的key,如果不确定,也推荐使用allkeys-lru策略。
-
- 如果所有key访问频率都差不多,那么可以选择allkeys-random策略,随机淘汰。
-
- 如果对数据有足够了解,能够为key指定命中,那么可以选择volatile-ttl进行置换。
2.5、修改缓存配置
config set maxmemory-policy allkeys-lru
或者修改配置文件,maxmemory-policy allkeys-lru
三、BigKey
3.1、插入100W条记录
for i in {1..1000000}
do echo "set key$i $i" >> file.txt;
donecat file.txt | redis-cli -a 18302679697 --pipe
3.2、查看是否插入成功
keys * #别使用该命令,数据量太大,会导致阻塞
DBSIZE
*实际使用中,禁用keys ,flushdb,flushall等命令
通过配置设置禁用这些命令,redis.conf中:SECURITY中
rename command keys ""
rename command flushdb ""
rename command flushall ""
3.3、使用SCAN之类命令
SCAN,SSCAN,HSCAN,ZSCAN一组,分别针对不同的数据类型
SCAN是基于游标的迭代器,每次调用之后,都会返回一个新的游标,直到返回0为止。
SCAN cursor [MATCH pattern] [COUNT count] #扫描游标,匹配模式,计数器,类似MySQL中的Limit#上一步的返回,是下一次扫描的游标,0表示结束,也代表开始
SCAN 0
20
scan 20
- 返回值有两个
- 下次遍历的新游标
- 数组,包含被迭代的元素
- SCAN的遍历顺序
并不是从第一位开始遍历,而是采用高位进位加法的方式遍历,之所以使用这样的方式进行遍历,是考虑字典的扩容和缩容时避免槽位的遍历重复和遗漏。
3.4、多大算BigKey?
首先大的不是key本身,而是value.
- string类型控制在10KB以内
- 哈希类型控制在5000个字段以内
- 列表类型控制在5000个元素以内
- 集合类型控制在5000个元素以内
- 排序类型控制在5000个元素以内
3.5、BigKey的危害
- 内存不均,集群迁移困难
- 超时删除,大key阻塞
- 网络流量阻塞
3.6、如何会产生BigKey?
- 社交类:
- 粉丝列表,某明星的粉丝,逐步递增
- 汇总统计:
- 某个报表,经年累月积累
3.7、如何发现BigKey?
-
redis-cli --bigkeys
- 好处:给出每种数据结构Top1 BigKey,同时给出每种数据结构的键值个数 + 平均大小
- 不足:想查询大于10kb的所有key,该命令无能为力
-
MEMORY USAGE key
给出一个key和它的值在RAM中所占用的字节数,返回的值时key的值以及为管理该key分配的内存总字节数
语法:
MEMORY USAGE key [SAMPLES count]
3.8、如何删除BigKey?
非字符串的bigkey,不要使用del删除,使用hscan,sscan,zscan等命令逐步删除;同时也要注意防止bigkey过期时间自动删除。
- string类型: 一般可用del,如果过于庞大unlink
- hash类型:使用hscan每次获取少量field-value,再使用hdel删除每个field,游标从0开始,直到返回游标为0,do-while循环
- list类型:使用LTRIM类似
- set类型:使用sscan类似
- zset类型:使用zsacn类似,再使用ZREMRANGERBYSCORE删除命令删除每个元素
3.9、BigKey调优
redis.conf配置文件中LAZY FREEING相关说明
lazyfree-lazy-eviction no #默认关闭,当内存达到上限时,释放对象采用延迟策略
lazyfree-lazy-expire no #默认关闭,过期删除采用延迟策略
lazyfree-lazy-server-del yes #默认关闭,删除键值对采用延迟策略
replacer-lazy yes #默认关闭,删除键值对采用延迟策略
lazyfree-lazy-user-del yes #默认关闭,用户主动删除键值对采用延迟策略
四、总结
4.1、Redis内存淘汰策略
- 2个维度:过期键中筛选,所有键中筛选
- 4个方面:LRU,LFU,随机,TTL
总计8种策略。
4.2、redis有这些缓存策略,是不是可以随意存储bigkey?
虽然redis对过期键有3三种删除方式,又增添了8种策略来淘汰内存,但不代表就可以在redis中存储bigkey数据,会极大影响redis的性能,甚至导致服务不可用。
Code
0vice·GitHub