常用的 SQL 数据库的数据都是存在磁盘中的,虽然在数据库底层也做了对应的缓存来减少数据库的 IO 压力。
由于数据库的缓存一般是针对查询的内容,而且粒度也比较小,一般只有表中的数据没有发生变动的时候,数据库的缓存才会产生作用。
但这并不能减少业务逻辑对数据库的增删改操作的 IO 压力,因此缓存技术应运而生,该技术实现了对热点数据的高速缓存,可以大大缓解后端数据库的压力。
主流应用架构
客户端在对数据库发起请求时,先到缓存层查看是否有所需的数据,如果缓存层存有客户端所需的数据,则直接从缓存层返回,否则进行穿透查询,对数据库进行查询。
如果在数据库中查询到该数据,则将该数据回写到缓存层,以便下次客户端再次查询能够直接从缓存层获取数据。
缓存中间件 Memcache 和 Redis 的区别
Memcache 的代码层类似 Hash,特点如下:
- 支持简单数据类型
- 不支持数据持久化存储
- 不支持主从
- 不支持分片
Redis 特点如下:
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从
- 支持分片
为什么 Redis 能这么快
- Redis 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。
- Redis 使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘 IO 的限制,因此其执行速度极快。
另外单线程也能处理高并发请求,还可以避免频繁上下文切换和锁的竞争,如果想要多核运行也可以启动多个实例。
- 数据结构简单,对数据操作也简单,Redis 不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于 HashMap,HashMap 最大的优点就是存取的时间复杂度为 O(1)。
- Redis 使用多路 I/O 复用模型,为非阻塞 IO。
- 因地制宜,优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现。
- 由于 Select 要遍历每一个 IO,所以其时间复杂度为 O(n),通常被作为保底方案。
- 基于 React 设计模式监听 I/O 事件。
Redis 的数据类型
最基本的数据类型,其值最大可存储 512M,二进制安全(Redis 的 String 可以包含任何二进制数据,包含 jpg 对象等)。
注:如果重复写入 key 相同的键值对,后写入的会将之前写入的覆盖。
String 元素组成的字典,适用于存储对象。
列表,按照 String 元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。
String 元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为 O(1)),不允许重复。
通过分数来为集合中的成员进行从小到大的排序。
从海量 Key 里查询出某一个固定前缀的 Key
假设 Redis 中有十亿条 Key,如何从这么多 Key 中找到固定前缀的 Key?
keys test* //返回所有以test为前缀的key
注:
- cursor:游标
- MATCH pattern:查询 Key 的条件
- Count:返回的条数
SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key
如何通过 Redis 实现分布式锁
- 互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。
- 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。
- 死锁:获取锁的客户端因为某些原因而宕机继而无法释放锁,其他客户端再也无法获取锁而导致死锁,此时需要有特殊机制来避免死锁。
- 容错:当各个节点,如某个 Redis 节点宕机的时候,客户端仍然能够获取锁或释放锁。
使用 SETNX 实现,SETNX key value:如果 Key 不存在,则创建并赋值。
该命令时间复杂度为 O(1),如果设置成功,则返回 1,否则返回 0。
用法:
EXPIRE key seconds
程序:
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key,"1");
if(status == 1){
redisService.expire(key,expire);
doOcuppiedWork();
}
- EX second:设置键的过期时间为 Second 秒。
- PX millisecond:设置键的过期时间为 MilliSecond 毫秒。
- NX:只在键不存在时,才对键进行设置操作。
- XX:只在键已经存在时,才对键进行设置操作。
SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
有了 SET 我们就可以在程序中使用类似下面的代码实现分布式锁了:
RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK.equals(result)"){
doOcuppiredWork();
}
如何实现异步队列
①使用 Redis 中的 List 作为队列
使用上文所说的 Redis 的数据结构中的 List 作为队列 Rpush 生产消息,LPOP 消费消息。
②使用 BLPOP key [key…] timeout
BLPOP key [key …] timeout:阻塞直到队列有消息或者超时。
③Pub/Sub:主题订阅者模式
发送者(Pub)发送消息,订阅者(Sub)接收消息。订阅者可以订阅任意数量的频道。
Redis 持久化
RDB持久化会在某个特定的间隔保存那个时间点的全量数据的快照。
RDB 配置文件,redis.conf:
save 900 1 #在900s内如果有1条数据被写入,则产生一次快照。
save 300 10 #在300s内如果有10条数据被写入,则产生一次快照
save 60 10000 #在60s内如果有10000条数据被写入,则产生一次快照
stop-writes-on-bgsave-error yes
#stop-writes-on-bgsave-error :
如果为yes则表示,当备份进程出错的时候,
主进程就停止进行接受新的写入操作,这样是为了保护持久化的数据一致性的问题。
①RDB 的创建与载入
②自动化触发 RDB 持久化的方式
自动化触发RDB持久化的方式如下:
- 根据 redis.conf 配置里的 SAVE m n 定时触发(实际上使用的是 BGSAVE)。
- 主从复制时,主节点自动触发。
- 执行 Debug Reload。
- 执行 Shutdown 且没有开启 AOF 持久化。
③BGSAVE 的原理
- 检查是否存在子进程正在执行 AOF 或者 RDB 的持久化任务。如果有则返回 false。
- 调用 Redis 源码中的 rdbSaveBackground 方法,方法中执行 fork() 产生子进程执行 RDB 操作。
- 关于 fork() 中的 Copy-On-Write。
④RDB 持久化方式的缺点
RDB 持久化方式的缺点如下:
- 内存数据全量同步,数据量大的状况下,会由于 I/O 而严重影响性能。
- 可能会因为 Redis 宕机而丢失从当前至最近一次快照期间的数据。
- AOF 记录除了查询以外的所有变更数据库状态的指令。
- 以增量的形式追加保存到 AOF 文件中。
一般来说,操作系统考虑效率问题,会等待缓冲区被填满再将缓冲区数据写入 AOF 文件中。
appendonly yes
#appendsync always
appendfsync everysec
# appendfsync no
- 调用 fork(),创建一个子进程。
- 子进程把新的 AOF 写到一个临时文件里,不依赖原来的 AOF 文件。
- 主进程持续将新的变动同时写到内存和原来的 AOF 里。
- 主进程获取子进程重写 AOF 的完成信号,往新 AOF 同步增量变动。
- 使用新的 AOF 文件替换掉旧的 AOF 文件。
AOF 和 RDB 的优缺点如下:
- RDB 优点:全量数据快照,文件小,恢复快。
- RDB 缺点:无法保存最近一次快照之后的数据。
- AOF 优点:可读性高,适合保存增量数据,数据不易丢失。
- AOF 缺点:文件体积大,恢复时间长。
Redis 数据的恢复
RDB 和 AOF 文件共存情况下的恢复流程如下图:
Pineline
Pipeline 和 Linux 的管道类似,它可以让 Redis 批量执行指令。
Redis 的同步机制
另外,Master 和 Slave 的数据不是一定要即时同步的,但是在一段时间后 Master 和 Slave 的数据是趋于同步的,这就是最终一致性。
全同步过程如下:
- Slave 发送 Sync 命令到 Master。
- Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中。
- Master 将保存数据快照期间接收到的写命令缓存起来。
- Master 完成写文件操作后,将该文件发送给 Slave。
- 使用新的 AOF 文件替换掉旧的 AOF 文件。
- Master 将这期间收集的增量写命令发送给 Slave 端。
增量同步过程如下:
- Master 接收到用户的操作指令,判断是否需要传播到 Slave。
- 将操作记录追加到 AOF 文件。
- 将操作传播到其他 Slave:对齐主从库;往响应缓存写入指令。
- 将缓存中的数据发送给 Slave。
- 监控:检查主从服务器是否运行正常。
- 提醒:通过 API 向管理员或者其它应用程序发送故障通知。
- 自动故障迁移:主从切换(在 Master 宕机后,将其中一个 Slave 转为 Master,其他的 Slave 从该节点同步数据)。
Redis 集群
①分片
②一致性 Hash 算法
如果定位到的地方没有 Redis 服务器实例,则继续顺时针寻找,找到的第一台服务器即该数据最终的服务器位置。
③Hash 环的数据倾斜问题
Hash 环在服务器节点很少的时候,容易遇到服务器节点不均匀的问题,这会造成数据倾斜,数据倾斜指的是被缓存的对象大部分集中在 Redis 集群的其中一台或几台服务器上。
针对这一问题,可以引入虚拟节点解决。简单地说,就是为每一个服务器节点计算多个 Hash,每个计算结果位置都放置一个此服务器节点,称为虚拟节点,可以在服务器 IP 或者主机名后放置一个编号实现。