Redisson 的分布式缓存结合 Redis 的持久化机制,为分布式系统提供了高性能的数据访问能力。但在多节点环境下,缓存与数据源(如数据库)之间的数据一致性是核心挑战。以下从缓存策略、一致性模型到具体实现方案详细说明:
一、缓存策略与一致性挑战
1. 常见缓存策略
- Cache-Aside:应用主动读写缓存和数据库(最常用)。
- Read-Through:读缓存缺失时自动加载数据。
- Write-Through:写操作同步更新缓存和数据库。
- Write-Behind:写操作异步更新缓存,定期批量落库。
2. 一致性挑战
- 读写并发:多个节点同时读写缓存和数据库,可能导致数据不一致。
- 网络分区:Redis 集群节点间通信中断,可能引发脑裂问题。
- 缓存失效:缓存过期或删除时机不当,导致脏数据。
二、Redisson 缓存实现方案
1. 基础 Cache-Aside 实现
java
RMap<String, Product> cache = redisson.getMap("product:cache",
MapOptions.<String, Product>defaults()
.timeToLive(30, TimeUnit.MINUTES) // 缓存30分钟
.maxIdle(10, TimeUnit.MINUTES)); // 最大空闲时间10分钟
// 读操作
public Product getProduct(String productId) {
// 先查缓存
Product product = cache.get(productId);
if (product != null) {
return product;
}
// 缓存缺失,查数据库
product = db.queryProduct(productId);
if (product != null) {
cache.put(productId, product); // 写入缓存
}
return product;
}
// 写操作
public void updateProduct(Product product) {
// 先更新数据库
db.updateProduct(product);
// 再删除缓存(避免更新失败导致脏数据)
cache.remove(product.getId());
}
2. 本地缓存(LocalCachedMap)
减少对 Redis 的访问,提升性能:
java
// 配置本地缓存 + 远程同步
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
.cacheSize(1000) // 本地缓存最大容量
.timeToLive(60, TimeUnit.SECONDS) // 缓存过期时间
.maxIdle(30, TimeUnit.SECONDS); // 最大空闲时间
RLocalCachedMap<String, Product> localCache = redisson.getLocalCachedMap(
"product:localCache", options);
// 使用方式与普通 RMap 相同
Product product = localCache.get(productId);
三、数据一致性保障方案
1. 最终一致性:异步更新
java
// 使用 Redisson 的 RTopic 实现发布订阅
RTopic<Product> productUpdateTopic = redisson.getTopic("product:update");
// 写操作(主节点)
public void updateProduct(Product product) {
// 1. 更新数据库
db.updateProduct(product);
// 2. 发布更新消息
productUpdateTopic.publish(product);
// 3. 延迟删除缓存(防止更新过程中旧数据被读取)
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(
redisson.getQueue("product:cache:evict"));
delayedQueue.offer(product.getId(), 10, TimeUnit.SECONDS);
}
// 订阅者(各节点)
productUpdateTopic.addListener(Product.class, (channel, product) -> {
// 更新本地缓存(如果使用了 LocalCachedMap)
localCache.put(product.getId(), product);
});
// 延迟队列消费者(删除缓存)
RQueue<String> evictQueue = redisson.getQueue("product:cache:evict");
while (true) {
String productId = evictQueue.take();
cache.remove(productId);
}
2. 强一致性:分布式锁+读写锁
java
// 获取商品的读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("product:lock:" + productId);
// 读操作(允许多个线程同时读)
public Product getProduct(String productId) {
RLock readLock = rwLock.readLock();
readLock.lock();
try {
Product product = cache.get(productId);
if (product == null) {
product = db.queryProduct(productId);
if (product != null) {
cache.put(productId, product);
}
}
return product;
} finally {
readLock.unlock();
}
}
// 写操作(排他锁)
public void updateProduct(Product product) {
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 1. 更新数据库
db.updateProduct(product);
// 2. 删除缓存
cache.remove(product.getId());
} finally {
writeLock.unlock();
}
}
3. 缓存失效策略优化
java
// 设置合理的过期时间和淘汰策略
RMapCache<String, Product> cache = redisson.getMapCache("product:cache");
cache.setMaxSize(10000); // 最大缓存条目数
cache.expireAfterWrite(30, TimeUnit.MINUTES); // 写入后30分钟过期
// 批量更新时使用联锁
RLock lock1 = redisson.getLock("product:lock:1");
RLock lock2 = redisson.getLock("product:lock:2");
RMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
multiLock.lock();
try {
// 批量更新多个产品
updateProducts();
// 批量删除缓存
cache.fastRemove("product1", "product2");
} finally {
multiLock.unlock();
}
四、Redis 集群与 Sentinel 配置
1. 集群模式配置
java
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000")
.addNodeAddress("redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002")
.setScanInterval(2000) // 集群状态扫描间隔
.setRetryAttempts(3) // 重试次数
.setRetryInterval(1000); // 重试间隔
RedissonClient redisson = Redisson.create(config);
2. Sentinel 高可用配置
java
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://127.0.0.1:26379")
.addSentinelAddress("redis://127.0.0.1:26380")
.setDatabase(0)
.setConnectTimeout(5000) // 连接超时时间
.setTimeout(3000) // 操作超时时间
.setRetryAttempts(3); // 重试次数
RedissonClient redisson = Redisson.create(config);
五、异常处理与降级策略
1. 缓存雪崩处理
java
// 缓存加载时使用不同的随机过期时间
Random random = new Random();
int expireTime = 30 * 60 + random.nextInt(600); // 30分钟基础上随机增加0-10分钟
// 设置缓存时使用随机过期时间
cache.put(productId, product, expireTime, TimeUnit.SECONDS);
2. 缓存穿透防护
java
// 使用布隆过滤器过滤不存在的键
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("product:bloom");
bloomFilter.tryInit(1000000, 0.01); // 预计100万商品,误判率1%
// 初始化布隆过滤器(预加载所有商品ID)
loadAllProductIdsToBloomFilter();
// 查询时先通过布隆过滤器检查
public Product getProduct(String productId) {
if (!bloomFilter.contains(productId)) {
return null; // 一定不存在,直接返回
}
// 正常查询缓存和数据库
Product product = cache.get(productId);
if (product == null) {
product = db.queryProduct(productId);
if (product != null) {
cache.put(productId, product);
} else {
// 缓存空值(防止穿透)
cache.put(productId, null, 5, TimeUnit.MINUTES);
}
}
return product;
}
六、总结与最佳实践
-
缓存策略选择:
- 读多写少:优先使用 Cache-Aside + 本地缓存。
- 强一致性场景:使用读写锁或分布式事务。
- 最终一致性场景:使用发布订阅 + 异步更新。
-
一致性保障:
- 写操作:先更新数据库,再删除缓存(避免更新失败导致脏数据)。
- 读操作:缓存缺失时加锁加载数据(防止击穿)。
-
性能优化:
- 使用本地缓存减少 Redis 访问。
- 设置合理的过期时间和淘汰策略。
- 批量操作时使用联锁或管道。
-
高可用性:
- 部署 Redis 集群或 Sentinel 保证可用性。
- 实现熔断降级机制(如 Hystrix)应对 Redis 故障。
通过 Redisson 的分布式缓存与一致性保障方案,可在提升系统性能的同时,最大限度降低数据不一致风险。