1. 持久化
尽管Redis是一个内存数据库,但它支持两种持久化机制:RDB(快照持久化)和AOF(追加文件),这两种机制可以将数据写入磁盘,从而避免因进程退出而导致的数据丢失。
1.1 RDB(快照持久化)
RDB持久化是将当前内存中的数据生成快照并保存到硬盘的过程。就像拍照一样,RDB记录的是某一时刻内存中数据的状态。
RDB的特点
- 全量快照:RDB会将内存中的所有数据记录到磁盘中。
- 生成方式:
SAVE
:在主线程中执行,会导致阻塞,建议在非高峰期使用。BGSAVE
:创建子进程进行快照,主线程不阻塞,适合线上环境。
RDB的自动触发
除了手动触发外,Redis还可以在以下情况下自动生成RDB文件:
- 当数据在m秒内发生n次修改时,自动触发bgsave。
- 从节点执行全量复制时,主节点会自动执行bgsave。
- 执行
DEBUG RELOAD
命令时,会自动触发RDB操作。 - 执行
SHUTDOWN
命令时,如果未开启AOF持久化功能,会自动执行bgsave。
RDB的优缺点
优点:
- RDB是压缩的二进制文件,适合备份和全量复制。
- 加载RDB文件恢复数据的速度快。
缺点:
- RDB不支持实时持久化,可能导致数据丢失。
- 文件格式不兼容,老版本的Redis可能无法加载新版本的RDB文件。
1.2 AOF(追加文件)
AOF持久化以日志的方式记录每次写操作,重启时通过执行AOF文件中的命令来恢复数据。AOF的优点是可以实现更高的实时性。
AOF的工作流程
AOF的工作流程主要包括命令写入、文件同步、文件重写和重启加载。
- 命令写入:AOF以RESP文本协议格式记录每条写命令。
- 文件同步:Redis提供多种同步策略:
always
:每次写命令后立即同步。everysec
:每秒同步一次(默认设置)。no
:由操作系统决定何时同步,性能较高但不安全。
- 文件重写:定期重写AOF文件以减少文件大小。
- 重启加载:重启时优先加载AOF文件,其次加载RDB文件。
AOF的优缺点
优点:
- AOF支持实时持久化,数据丢失风险低。
- 文件格式是文本格式,易于查看和修改。
缺点:
- AOF文件通常比RDB文件大,加载速度慢。
- 频繁的命令写入可能导致AOF文件膨胀。
1.3 RDB与AOF的混合持久化
1. 混合持久化的背景
在Redis中,RDB(快照持久化)和AOF(追加文件)各有优缺点。RDB适合快速恢复,但可能会丢失最后几秒的数据,而AOF则可以提供更高的数据安全性,但在恢复时速度较慢。通过混合使用这两种持久化机制,可以有效地结合它们的优势,提高数据恢复的效率和安全性。
2. 混合持久化的场景
场景示例:电商系统的订单处理
在一个电商系统中,用户下单时需要频繁地对订单数据进行写操作。为了确保数据的安全性和快速恢复,系统采用了混合持久化的方式。
具体场景:
- 高频写入:用户在下单时会频繁地更新库存、订单状态等数据。
- 实时性需求:需要确保订单数据的即时性,避免数据丢失。
- 系统恢复:在系统崩溃或重启时,需要快速恢复到最近的状态。
3. 混合持久化的流程
-
开启混合持久化:
- 在Redis配置文件中设置
aof-use-rdb-preamble yes
,表示启用混合持久化。
- 在Redis配置文件中设置
-
触发RDB快照:
- Redis定期生成RDB快照,保存当前内存数据的状态。例如,设置每隔60秒或每100次写操作生成一次快照。
-
生成AOF文件:
- 在每次写操作后,Redis会将写命令追加到AOF文件中,同时在AOF文件的开头包含最近生成的RDB快照数据。
-
系统崩溃后的恢复:
- 当Redis重启时,首先读取AOF文件的开头部分,加载RDB快照数据,然后执行AOF文件中的写命令,以恢复到崩溃前的状态。
4. 混合持久化的好处
-
快速恢复:通过在AOF文件开头包含RDB快照数据,系统可以快速加载最近的快照,减少重启时的恢复时间。
-
数据安全性:AOF文件记录了所有的写操作,能够提供更高的实时性,确保即使在系统崩溃后也能恢复到最近的状态。
-
资源利用:混合持久化可以减少AOF文件的大小,因为在AOF中首先加载RDB快照,后续只需记录增量数据,降低了存储和I/O开销。
-
灵活性:用户可以根据业务需求调整RDB和AOF的结合方式,以适应不同的场景和性能要求。
2. Redis分布式锁
分布式锁是一种用于控制多个分布式系统中对共享资源的访问的机制。在使用Redis实现分布式锁时,确保锁的互斥性和安全性是非常重要的。
2.1 分布式锁的基本实现
Redis可以通过SETNX
命令实现分布式锁。该命令表示“如果不存在则设置”,可以用来实现互斥。
1. 加锁
客户端1尝试加锁成功,客户端2尝试加锁失败:
SETNX lock_key 1 # 客户端1加锁,成功则返回1
2. 释放锁
通过DEL
命令释放锁:
DEL lock_key # 客户端1释放锁
2.2 避免死锁
为避免死锁,可以在加锁时设置一个过期时间,例如10秒:
SET lock_key 1 EX 10 NX # 设置锁,过期时间为10秒
这样,即使客户端异常,锁也会在10秒后自动释放。
2.3 原子性问题
加锁和设置过期时间是两条命令,可能会出现网络问题导致第二条命令未执行。为了解决这个问题,可以使用Redis 2.6.12版本后扩展的SET
命令:
SET lock_key 1 EX 10 NX # 原子性地加锁并设置过期时间
2.4 释放锁的安全性
在释放锁时,需检查锁是否仍归自己持有。可以使用Lua脚本来确保原子性:
lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
2.5 Java实现分布式锁
以下是使用Jedis实现分布式锁的Java代码示例,代码中包含详细注释以帮助理解:
java
package com.hmm.redis.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 分布式锁的实现
*/
@Component
public class RedisDistLock implements Lock {
// 锁的过期时间
private static final int LOCK_TIME = 5000; // 5秒
private static final String LOCK_NAMESPACE = "lock:"; // 锁的命名空间
// Lua脚本,用于安全释放锁
private static final String RELEASE_LOCK_LUA =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
// 线程本地变量,存储每个线程的唯一ID
private ThreadLocal<String> lockerId = new ThreadLocal<>();
// 当前持有锁的线程
private Thread ownerThread;
private String lockName = "lock"; // 锁的名称
@Autowired
private JedisPool jedisPool; // Redis连接池
@Override
public void lock() {
// 循环尝试获取锁,直到成功
while (!tryLock()) {
try {
Thread.sleep(100); // 休眠100毫秒后重试
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public boolean tryLock() {
Thread currentThread = Thread.currentThread(); // 获取当前线程
// 如果当前线程已经持有锁,直接返回true
if (ownerThread == currentThread) {
return true;
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource(); // 从连接池获取Redis连接
String id = UUID.randomUUID().toString(); // 生成唯一ID
SetParams params = new SetParams().px(LOCK_TIME).nx(); // 设置过期时间和NX参数
// 尝试加锁
if ("OK".equals(jedis.set(LOCK_NAMESPACE + lockName, id, params))) {
lockerId.set(id); // 设置线程本地变量
ownerThread = currentThread; // 设置当前线程为锁的持有者
return true; // 加锁成功
}
} finally {
if (jedis != null) {
jedis.close(); // 关闭Redis连接
}
}
return false; // 加锁失败
}
@Override
public void unlock() {
// 检查当前线程是否为锁的持有者
if (ownerThread != Thread.currentThread()) {
throw new RuntimeException("Attempt to release a lock that is not owned!");
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource(); // 从连接池获取Redis连接
// 执行Lua脚本释放锁
Long result = (Long) jedis.eval(RELEASE_LOCK_LUA,
Arrays.asList(LOCK_NAMESPACE + lockName), // 锁的名称
Arrays.asList(lockerId.get())); // 当前线程的唯一ID
if (result != 0) {
System.out.println("Lock released successfully!"); // 释放成功
} else {
System.out.println("Failed to release lock!"); // 释放失败
}
} finally {
if (jedis != null) {
jedis.close(); // 关闭Redis连接
}
lockerId.remove(); // 清除线程本地变量
ownerThread = null; // 清除持有者线程
}
}
// 其他未实现的方法省略
}
3. 锁过期时间评估
如果业务逻辑执行时间超过锁的过期时间,就可能导致锁失效。为了解决这个问题,可以引入一个“看门狗”线程,定期检查锁的有效性并续期。
3.1 看门狗线程的实现
看门狗线程会在锁快到期时重新设置过期时间,以确保锁不会提前失效。
4. Redisson中的分布式锁
Redisson是一个Redis客户端,提供了封装好的分布式锁实现,简化了分布式锁的使用。以下是Redisson的使用示例:
java
RLock lock = redisson.getLock("lock");
lock.lock(10, TimeUnit.SECONDS); // 加锁,10秒后自动解锁
5. Redlock的实现
Redlock是一种在多个Redis实例上实现分布式锁的方案,适用于高可用场景。其主要步骤包括:
- 获取当前时间戳T1。
- 向多个Redis实例发送加锁请求。
- 如果大多数实例加锁成功,则认为加锁成功。
- 释放锁时,向所有节点发起解锁请求。
5.1 Redlock的安全性
Redlock的安全性体现在以下几个方面:
- 网络延迟和进程暂停的问题可以通过合理的超时机制来解决。
- 时钟漂移可能导致锁失效,因此需要在时钟同步方面加以注意。