通常地,再内存足够的情况下,使用过期策略对 key 的过期进行管理,而当内存使用达到限制时,会使用内存淘汰 进行管理
参考: redis expire
定期删除+惰性删除
redis 在使用时,可以对一个 key 设置过期时间(设置了过期时间的key,一般称为 volatile),到期之后怎么删除呢?使用 定期删除+惰性删除
shellEXPIRE <KEY> <TTL> : 将 key 的生存时间设置为 ttl 秒 PEXPIRE <KEY> <TTL> : 将 key 的生存时间设置为 ttl 毫秒 EXPIREAT <KEY> <timestamp> : 将 key 的过期时间设为 timestamp 所指定的秒数时间戳 PEXPIREAT <KEY> <timestamp> : 将 key 的过期时间设为 timestamp 所指定的毫秒数时间戳 TTL <KEY> : 返回剩余生存时间 ttl 秒 PTTL <KEY> : 返回剩余生存时间 pttl 毫秒 RERSIST <KEY> : 移除一个键的过期时间
Redis keys are expired in two ways: a passive way, and an active way
A key is passively expired simply when some client tries to access it, and the key is found to be timed out.
当客户端访问一个 key 时,会先检查是否存在过期时间?是否已经过期?如果过期则删除
如果只使用 passive way
,那么会导致有些过期后,不再被访问的 key
会一直留在内存空间。于是就有了 定期删除
的方式
This means that at any given moment the maximum amount of keys already expired that are using memory is at max equal to max amount of write operations per second divided by 4.
参考:lru-cache
在 redis 配置中,通过使用 maxmemory
配置 redis 使用内存大小的现在,可以通过 启动前配置配置文件 redis.conf,或者 启动后执行命令 CONFIG SET
来进行设置
如果配置为 0,则在内存使用上没有限制(在 64 位系统上来讲是这样的,32位会有隐式限制为3GB)
当内存使用达到配置限制时,会根据策略的选择做不同的处理
使用 maxmemory-policy
配置,配置策略
所以内存会不停地在内存限制区,不停地超出限制和返回限制下。如果提交一个大的数据,在某些时候会造成内存使用明显超出限制。
步骤:
画图:
对于 LRU 的数据结构,需要做到的是,保存有序,移动节点,添加节点(头结点),删除节点(尾节点),一般用于缓存所以需要保证查找的算法复杂度足够低。一般常用 双向链表 + Hash散列 来作为 LRU 的数据结构(这个结构也是 java 集合中 LinkedHashMap 数据结构的实现)
简单实现
javapackage com.yui.study.algorithms.base.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.HashMap;
import java.util.Map;
/**
* @author YUI_HTT
* @date 2021/3/30
*/
public class LruCache<K, V> {
private Entry<V> head, tail;
int capacity;
int size;
private Map<K, Entry<V>> cache;
@Data
@Accessors(chain = true)
public static class Entry<T> {
private Entry<T> preNode;
private Entry<T> nextNode;
private T value;
}
public LruCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.head = new Entry<>();
this.tail = new Entry<>();
this.head.nextNode = this.tail;
this.tail.preNode = head;
cache = new HashMap<>(capacity);
}
/**
* 放入元素
* @param key
* @param value
*/
public void put(K key, V value){
Entry<V> newNode = this.cache.get(key);
if (newNode != null){
moveToHead(newNode);
newNode.value = value;
return;
}
if (size == capacity){
removeTail(key);
}
newNode = new Entry<>();
newNode.value = value;
addNode(newNode);
this.cache.put(key, newNode);
size++;
}
public V get(K key){
Entry<V> node = this.cache.get(key);
if (node == null){
return null;
}
moveToHead(node);
return node.value;
}
public void moveToHead(Entry<V> node){
deleteNode(node);
addNode(node);
}
public void removeTail(K key){
deleteNode(this.tail.preNode);
this.cache.remove(key);
size--;
}
public void addNode(Entry<V> node){
this.head.nextNode.preNode = node;
node.preNode = this.head;
node.nextNode = this.head.nextNode;
this.head.nextNode = node;
}
public void deleteNode(Entry<V> node){
node.preNode.nextNode = node.nextNode;
node.nextNode.preNode = node.preNode;
}
public void display() {
Entry<V> currentNode = this.head.nextNode;
if(currentNode == null) {
return;
}
while (currentNode.nextNode != null) {
System.out.print(currentNode.value + "\t");
currentNode = currentNode.nextNode;
}
System.out.println("");
}
public static void main(String[] args) {
LruCache<String, String> cache = new LruCache<>(5);
cache.put("test1", "1");
cache.put("test2", "2");
cache.put("test3", "3");
cache.put("test4", "4");
cache.put("test5", "5");
cache.display();
System.out.println(cache.get("test3"));
cache.display();
System.out.println(cache.get("test5"));
cache.display();
cache.put("test6", "6");
cache.display();
}
}
运行结果如下:
text5 4 3 2 1 3 3 5 4 2 1 5 5 3 4 2 1 6 5 3 4 2
数据结构 | 内部编码 | 内部结构 | 优劣性 |
---|---|---|---|
string | int | 8 个字节的 长整型 | |
embstr | 小于等于 39 个字节的字符串 | ||
raw | 大于 39 个字节的字符串 | ||
hash | ziplist | 1.元素个数小于配置hash-max-ziplist-entries (默认512 )2.所有的值小于配置 hash-max-ziplist-value 配置(默认 64 字节)时同时满足1,2点 | 更加 紧凑的结构 实现多个元素的 连续存储,使用内存更少 |
hashtable | 无法满足 ziplist 的条件时 | 读写效率更高 | |
list | ziplist | 1.元素个数小于配置 list-max-ziplist-entries (默认 512 个)2.同时列表中 每个元素 的值都 小于 list-max-ziplist-value 配置时(默认 64 字节)同时满足1,2点 | 更加 紧凑的结构 实现多个元素的 连续存储,使用内存更少 |
linkedlist | 无法满足 ziplist 的条件时 | ||
set | intset | 1.元素都是整数 2.元素个数小于配置 set-max-intset-entries (默认 512 个)同时满足1,2点 | 使用内存更少 |
hashtable | 无法满足 intset 的条件时 | ||
zset | ziplist | ||
skiplist |
incr
lpush + lpop
lpush+rpop
lpush + ltrim
lpush + brpop
sadd
标签(tag):比如用户画像,人群分类,文章分类spop/srandmember
: 生成随机数,比如抽奖sadd + sinter
: 社交需求由客户端决定数据所属(写/读)的节点,一般是采用 hash 算法对 key 进行计算
客户端分区方案的代表为 Redis Sharding
, 这是在 Redis Cluster
出来之前,业界普遍使用的 Redis 多实例集群
方法。
java 的客户端驱动库 jedis
, 就是 Redis Sharding
+ 缓存池 ShardedJedisPool
优点
缺点
客户端 发送请求到一个 代理组件,代理 解析 客户端 的数据,并将请求转发至正确的节点,最后将结果回复给客户端
优点:
缺点:多了一层 代理层,加重了 架构部署复杂度 和 性能损耗。
代理分区 主流实现的有方案有 Twemproxy
和 Codis
。
客户端随机地请求任意一个 Redis
实例,然后由 Redis
将请求转发给正确的 Redis
节点.
Redis Cluster
实现了一种 混合形式 的 查询路由, 不是由节点转发,而是在 客户端 的帮助下直接 重定向(redirected)
Redis-trib
工具,缺乏 监控管理Smart Client
Failover
节点检测过慢,不如中心接线 zookeeper
及时Gossip
消息具有一定开销对于分布式数据库来讲,需要解决的一个重要问题就是,要解决 整个数据集 按照 分区规则 映射到 多个节点 的问题。即如何把数据集分配到多个节点上,由每个节点负责一个子集
数据的分布规则通常由 哈希分区 和 顺序分区 两种方式,其对比如下:
分区方式 | 特点 | 相关产品 |
---|---|---|
哈希分区 | 离散程度好 数据分布与业务无关 无法顺序访问 | Redis Cluster Cassandra Dynamo |
顺序分区 | 离散程度易倾斜 数据分布与业务相关 可以顺序访问 | BigTable HBase Hypertable |
Redis Cluster
使用的为 哈希分区,所以这里主要讨论 哈希分区
使用特定数据进行计算出 hash 值,再模以节点数,来得到数据落点
对于redis key 在 N 个节点的集群上可以计算为:hash(key) % N
针对这种方式的扩缩容:
优点:
缺点:
步骤如下:
扩缩容:
扩容时需要计算新节点的落点,该落点的数据(落点前一个节点A到落点后一个节点B区间的数据)本为B节点管理,新增节点后需要调整为A节点到新落点的数据为新落点管理,新落点的到B节点的数据为B节点管理。即:分担落点的后一个相邻节点的数据
graph LR;
T1[ ] --- A
A[节点A] --- D1[data1]
D1 --- D2[data2]
D2 --- D3[data3]
D3 --- D4[data4]
D4 --- B[节点B]
B --- T2[ ]
graph LR;
T1[ ] --- A
A[节点A] --- D1[data1]
D1 --- D2[data2]
D2 --- C[节点C]
C --- D3[data3]
D3 --- D4[data4]
D4 --- B[节点B]
B --- T2[ ]
缩容也是则相反的,把要释放的节点负责的数据交由后一个相邻节点管理
所以扩容和缩容所影响的只有对应节点的后一个相邻节点
优点:
缺点:
经常会通过采用虚拟节点(即,每台机都视为一组虚拟节点,同时加入 hash 环中)来增加节点间的复杂均衡,减少一致性哈希算法的缺点,而 redis 则采用虚拟槽的方式
见章节[5.2.2 redis的虚拟槽分区](#5.2.2 redis的虚拟槽分区)
虚拟槽分区 通过使用 分散度良好 的 哈希函数(CRC16) 把所有数据 映射 到一个 固定范围 的 整数集合 中,该集合称为 槽(slot
) 。
Redis Cluster
槽范围为 0 ~ 16383
CRC16(key) % 16383
功能限制:
key
批量操作 支持有限:只支持在相同 solt 操作,如 mset, mgetkey
为分区的最小粒度,一个大数据的key,不能被分配到不同的节点db0
布隆过滤: 用于判断数据是否不存在
主要构成:
算法:
由于布隆过滤的特性,导致保存在布隆过滤中的数据越多,计算出的结果越容易命中,即使数据不存在布隆过滤中也会被命中,所以布隆过滤主要是保证过滤不命中的数据
布隆过滤使用场景:
Redis 提供了 RDB 和 AOF 两种方式来实现数据的持久化存储
redis.conf
中配置了 save m n
,表示如果在 m 秒内存在了 n 次修改操作时,则自动触发bgsave
;bgsave
,并将生成的 RDB 文件发送给从节点;debug reload
命令重新加载 Redis 时,会触发save
操作;shutdown
命令时候,如果没有启用 AOF 持久化则默认采用bgsave
进行持久化。redis.conf
中的工作目录dir
和数据库存储文件名dbfilename
两个配置config set dir{newDir} config
, set dbfilename{newFileName}
redis.conf
中的 rdbcompression
配置config set rdbcompression{yes|no}
命令以独立日志的方式记录每次写入操作,重启时再重新执行这些操作,从而达到恢复数据的命令
appendfsync
appendonly
的值为yes
,默认值为no
。appendonly.aof
, 可以通过修改appendfilename
的值进行修改,和 RDB 文件的保存位置一样,默认保存在 Redis 的工作目录下RDB 优缺点
优点:
缺点:
实时性低,在持久化间隔间会宕机会出现数据丢失
fork 操作是重量级操作,数据大时, fork 操作非常耗时
AOF 优缺点
按照 Redis 官方的推荐,为保证的数据安全性,可以同时使用这两种持久化机制,在 Redis 官方的长期计划里面,未来可能会将 AOF 和 RDB 统一为单一持久化模型。
需要注意的是,当 Redis 重新启动时,Redis 将使用 AOF 文件重建数据集,因为它可以保证数据的最少丢失。
本文作者:Yui_HTT
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!