缓存一致性方案:pig平台Redis与数据库双写一致性
【免费下载链接】pig 项目地址: https://gitcode.com/gh_mirrors/pig/pig
1. 引言:缓存双写一致性的痛点与挑战
你是否在分布式系统开发中遇到过以下问题:用户明明更新了数据,页面却显示旧内容?订单状态已经支付,后台查询却显示未支付?这些令人头疼的问题往往源于缓存(Cache)与数据库(Database)之间的数据一致性问题。在高并发场景下,如何保证Redis缓存与数据库的数据同步,成为影响系统可靠性的关键因素。
本文将以pig微服务平台为实际案例,深入剖析缓存双写一致性的六大实现方案,重点解读pig平台如何通过Spring Cache注解+Redis实现高效可靠的缓存一致性策略,并提供完整的代码实现与最佳实践指南。
读完本文你将获得:
- 缓存与数据库一致性问题的根源分析
- 六种缓存更新策略的优缺点对比
- pig平台缓存一致性实现的源码级解析
- 基于Spring Cache的企业级缓存方案设计
- 缓存并发问题的解决方案与性能优化技巧
2. 缓存一致性问题的技术本质
2.1 数据不一致的三大场景
缓存与数据库的数据不一致通常发生在以下三种场景:
2.2 一致性问题的数学模型
缓存一致性问题本质上是分布式系统中的最终一致性挑战。我们可以用以下公式定义缓存一致性的程度:
一致性程度 = 1 - (缓存脏数据量 / 总数据量) * 不一致持续时间
理想情况下,我们希望一致性程度达到100%,但这在实际工程中往往需要在性能与一致性之间做出权衡。
3. 缓存更新策略全景对比
3.1 六大缓存更新策略对比表
| 策略名称 | 实现方式 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| Cache-Aside | 读:缓存有则读缓存,无则读库并回写缓存 写:先更库,后删缓存 | 高 | 高 | 低 | 大多数读多写少场景 |
| Read-Through | 读操作通过缓存抽象层,缓存未命中时自动加载数据库数据 | 中 | 中 | 中 | 底层存储访问复杂的场景 |
| Write-Through | 写操作通过缓存抽象层,同步更新缓存和数据库 | 高 | 低 | 高 | 强一致性要求场景 |
| Write-Behind | 写操作先更新缓存,异步批量更新数据库 | 低 | 高 | 极高 | 非实时数据统计场景 |
| Cache-Update | 先更新缓存,后更新数据库 | 低 | 高 | 低 | 极少使用,仅适用于临时数据 |
| TTL自动过期 | 设置缓存过期时间,依赖过期机制更新 | 低 | 高 | 极低 | 容忍短期不一致的场景 |
3.2 各策略的时序图对比
Cache-Aside策略时序图:
Write-Through策略时序图:
4. pig平台缓存一致性方案实现
4.1 pig平台缓存架构概览
pig平台采用多级缓存架构,结合本地缓存(Caffeine)和分布式缓存(Redis),实现高性能与高一致性的平衡:
4.2 Spring Cache + Redis实现方案
pig平台主要基于Spring Cache注解结合Redis实现缓存一致性,核心注解包括:
@Cacheable: 缓存查询结果@CacheEvict: 清除缓存@CachePut: 更新缓存@Caching: 组合缓存操作@CacheConfig: 类级缓存配置
4.3 核心代码实现解析
4.3.1 缓存常量定义
/**
* 缓存常量定义
*/
public interface CacheConstants {
// 用户信息缓存
String USER_DETAILS = "user_details";
// 字典缓存
String DICT_DETAILS = "dict_details";
// 菜单缓存
String MENU_DETAILS = "menu_details";
// 角色缓存
String ROLE_DETAILS = "role_details";
// 参数缓存
String PARAMS_DETAILS = "params_details";
// 客户端缓存
String CLIENT_DETAILS_KEY = "client_details";
// 项目oauth缓存前缀
String PROJECT_OAUTH_ACCESS = "pig_oauth:access";
}
4.3.2 用户信息缓存实现
@Service
public class SysUserServiceImpl implements SysUserService {
private final SysUserMapper sysUserMapper;
private final RedisTemplate<String, Object> redisTemplate;
// 构造函数注入依赖...
/**
* 根据用户名查询用户信息,使用缓存
*/
@Override
@Cacheable(value = CacheConstants.USER_DETAILS, key = "#username", unless = "#result == null")
public SysUser getUserByUsername(String username) {
SysUser user = sysUserMapper.selectOne(new QueryWrapper<SysUser>()
.lambda().eq(SysUser::getUsername, username));
if (user == null) {
return null;
}
// 获取用户角色信息
List<String> roles = sysRoleService.findRolesByUserId(user.getUserId());
user.setRoles(roles);
return user;
}
/**
* 更新用户信息,清除缓存
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
public Boolean updateUser(UserDTO userDto) {
SysUser user = new SysUser();
BeanUtils.copyProperties(userDto, user);
return sysUserMapper.updateById(user) > 0;
}
/**
* 删除用户,清除缓存
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#username")
public Boolean removeUser(String username) {
return sysUserMapper.delete(new QueryWrapper<SysUser>()
.lambda().eq(SysUser::getUsername, username)) > 0;
}
}
4.3.3 字典缓存实现
@RestController
@RequestMapping("/dict")
public class SysDictController {
private final SysDictService sysDictService;
// 构造函数注入依赖...
/**
* 根据字典类型查询字典数据,使用缓存
*/
@GetMapping("/type/{type}")
@Cacheable(value = CacheConstants.DICT_DETAILS, key = "#type", unless = "#result.data.isEmpty()")
public R<List<SysDictItem>> getDictByType(@PathVariable String type) {
List<SysDictItem> dictItems = sysDictService.getDictByType(type);
return R.ok(dictItems);
}
/**
* 新增字典,清除所有字典缓存
*/
@PostMapping
@CacheEvict(value = CacheConstants.DICT_DETAILS, allEntries = true)
public R<Boolean> saveDict(@Valid @RequestBody SysDict sysDict) {
return R.ok(sysDictService.save(sysDict));
}
/**
* 更新字典,清除对应类型的字典缓存
*/
@PutMapping
@CacheEvict(value = CacheConstants.DICT_DETAILS, key = "#sysDict.dictType")
public R<Boolean> updateDict(@Valid @RequestBody SysDict sysDict) {
return R.ok(sysDictService.updateById(sysDict));
}
}
4.4 缓存一致性保障机制
pig平台通过以下多层机制保障缓存一致性:
- 延迟双删策略:在更新数据库后,先删除缓存,延迟一段时间(如500ms)后再次删除缓存,解决缓存更新过程中的并发问题。
/**
* 延迟双删工具类
*/
@Component
public class CacheDelayUtils {
private final RedisTemplate<String, Object> redisTemplate;
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
// 构造函数注入依赖...
/**
* 延迟双删
* @param key 缓存key
* @param delay 延迟时间(毫秒)
*/
public void delayDoubleDelete(String key, long delay) {
// 立即删除
redisTemplate.delete(key);
// 延迟删除
executorService.schedule(() -> redisTemplate.delete(key), delay, TimeUnit.MILLISECONDS);
}
/**
* 延迟双删(默认延迟500ms)
* @param key 缓存key
*/
public void delayDoubleDelete(String key) {
delayDoubleDelete(key, 500);
}
}
- 缓存过期时间设置:为所有缓存设置合理的过期时间,作为防止缓存不一致的最后一道防线。
/**
* Redis缓存配置
*/
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默认缓存过期时间1小时
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 不同缓存设置不同的过期时间
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put(CacheConstants.USER_DETAILS, defaultConfig.entryTtl(Duration.ofMinutes(30)));
configs.put(CacheConstants.DICT_DETAILS, defaultConfig.entryTtl(Duration.ofHours(24)));
configs.put(CacheConstants.MENU_DETAILS, defaultConfig.entryTtl(Duration.ofHours(12)));
configs.put(CacheConstants.ROLE_DETAILS, defaultConfig.entryTtl(Duration.ofHours(12)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configs)
.transactionAware()
.build();
}
}
- 缓存更新重试机制:对于缓存删除失败的情况,通过重试机制确保最终删除成功。
/**
* 缓存重试删除工具类
*/
@Component
public class CacheRetryUtils {
private final RedisTemplate<String, Object> redisTemplate;
private static final Logger log = LoggerFactory.getLogger(CacheRetryUtils.class);
// 构造函数注入依赖...
/**
* 带重试机制的缓存删除
* @param key 缓存key
* @param retryCount 重试次数
* @param interval 重试间隔(毫秒)
* @return 是否删除成功
*/
public boolean deleteWithRetry(String key, int retryCount, long interval) {
for (int i = 0; i <= retryCount; i++) {
try {
Boolean result = redisTemplate.delete(key);
if (Boolean.TRUE.equals(result)) {
return true;
}
if (i < retryCount) {
Thread.sleep(interval);
}
} catch (Exception e) {
log.error("缓存删除失败,key: {}, 重试次数: {}", key, i, e);
if (i < retryCount) {
try {
Thread.sleep(interval);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return false;
}
}
}
}
return false;
}
}
5. pig平台缓存性能优化实践
5.1 缓存粒度控制策略
pig平台根据数据访问特征,采用不同的缓存粒度:
- 粗粒度缓存:适用于变动频率低、访问频率高的数据,如字典表、配置参数等。
// 粗粒度缓存示例:缓存整个字典类型的数据
@Cacheable(value = CacheConstants.DICT_DETAILS, key = "#type")
public List<SysDictItem> getDictByType(String type) {
return sysDictItemMapper.selectList(new QueryWrapper<SysDictItem>()
.lambda().eq(SysDictItem::getDictType, type)
.orderByAsc(SysDictItem::getSort));
}
- 细粒度缓存:适用于变动频率高但部分字段变动少的数据,如用户基本信息和权限信息分离缓存。
// 细粒度缓存示例:分别缓存用户基本信息和权限信息
@Cacheable(value = CacheConstants.USER_BASE_INFO, key = "#userId")
public UserBaseInfo getUserBaseInfo(Long userId) {
// 查询用户基本信息
}
@Cacheable(value = CacheConstants.USER_PERMISSIONS, key = "#userId")
public List<String> getUserPermissions(Long userId) {
// 查询用户权限信息
}
5.2 缓存穿透防护
pig平台通过布隆过滤器和空值缓存两种机制防止缓存穿透:
/**
* 布隆过滤器配置
*/
@Configuration
public class BloomFilterConfig {
@Bean
public BloomFilter<Long> userIdBloomFilter() {
// 预计数据量100万,误判率0.01
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
// 初始化加载所有用户ID到布隆过滤器
List<Long> userIds = sysUserMapper.selectObjs(new QueryWrapper<SysUser>()
.select("user_id").lambda())
.stream()
.map(obj -> (Long) obj)
.collect(Collectors.toList());
userIds.forEach(filter::put);
return filter;
}
}
/**
* 用户服务中的缓存穿透防护
*/
@Service
public class SysUserServiceImpl implements SysUserService {
private final BloomFilter<Long> userIdBloomFilter;
// 构造函数注入依赖...
@Override
public UserBaseInfo getUserBaseInfo(Long userId) {
// 布隆过滤器判断ID是否存在,不存在直接返回null
if (!userIdBloomFilter.mightContain(userId)) {
return null;
}
// 查询缓存
UserBaseInfo userBaseInfo = (UserBaseInfo) redisTemplate.opsForValue().get(
CacheConstants.USER_BASE_INFO + ":" + userId);
if (userBaseInfo == null) {
// 查询数据库
userBaseInfo = sysUserMapper.selectUserBaseInfoById(userId);
if (userBaseInfo != null) {
// 缓存存在的用户信息
redisTemplate.opsForValue().set(
CacheConstants.USER_BASE_INFO + ":" + userId,
userBaseInfo,
30, TimeUnit.MINUTES);
} else {
// 缓存空值,设置较短的过期时间
redisTemplate.opsForValue().set(
CacheConstants.USER_BASE_INFO + ":" + userId,
new UserBaseInfo(),
5, TimeUnit.MINUTES);
}
}
// 返回结果(如果是空对象则返回null)
return userBaseInfo.getId() == null ? null : userBaseInfo;
}
}
5.3 缓存击穿防护
pig平台通过互斥锁防止缓存击穿:
/**
* 缓存击穿防护工具类
*/
@Component
public class CacheBreakdownUtils {
private final StringRedisTemplate stringRedisTemplate;
// 构造函数注入依赖...
/**
* 获取缓存,带互斥锁机制防止缓存击穿
*/
public <T> T getWithLock(String key, Supplier<T> supplier, long expireTime, TimeUnit timeUnit) {
// 1. 尝试从缓存获取
String value = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
return JSON.parseObject(value, new TypeReference<T>() {});
}
// 2. 获取互斥锁
String lockKey = "lock:" + key;
boolean locked = false;
try {
locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 300, TimeUnit.SECONDS);
if (locked) {
// 3. 再次检查缓存,防止并发线程已经加载过数据
value = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
return JSON.parseObject(value, new TypeReference<T>() {});
}
// 4. 从数据库加载数据
T result = supplier.get();
// 5. 缓存数据
if (result != null) {
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(result), expireTime, timeUnit);
}
return result;
} else {
// 6. 获取锁失败,等待后重试
Thread.sleep(50);
return getWithLock(key, supplier, expireTime, timeUnit);
}
} catch (Exception e) {
log.error("缓存获取异常,key: {}", key, e);
return null;
} finally {
// 7. 释放锁
if (locked) {
stringRedisTemplate.delete(lockKey);
}
}
}
}
5.4 缓存雪崩防护
pig平台通过过期时间随机化和多级缓存防止缓存雪崩:
/**
* 带随机过期时间的缓存工具类
*/
@Component
public class RandomExpireCacheUtils {
private final RedisTemplate<String, Object> redisTemplate;
// 构造函数注入依赖...
/**
* 设置带随机过期时间的缓存
* @param key 缓存key
* @param value 缓存值
* @param baseExpire 基础过期时间
* @param randomRange 随机时间范围(正负)
* @param timeUnit 时间单位
*/
public void setWithRandomExpire(String key, Object value, long baseExpire, long randomRange, TimeUnit timeUnit) {
// 转换为毫秒
long baseMs = timeUnit.toMillis(baseExpire);
long randomMs = timeUnit.toMillis(randomRange);
// 生成随机过期时间
long randomOffset = (long) (Math.random() * randomMs * 2 - randomMs);
long actualExpireMs = baseMs + randomOffset;
redisTemplate.opsForValue().set(key, value, actualExpireMs, TimeUnit.MILLISECONDS);
}
}
6. 缓存一致性监控与运维
6.1 缓存监控指标体系
pig平台定义了完善的缓存监控指标,包括:
/**
* 缓存监控指标
*/
public interface CacheMetrics {
// 缓存命中率
double getCacheHitRate();
// 缓存穿透率
double getCachePenetrationRate();
// 缓存平均响应时间
double getAverageResponseTime();
// 缓存更新成功率
double getCacheUpdateSuccessRate();
// 缓存容量使用率
double getCacheCapacityUsage();
}
6.2 缓存监控实现
/**
* Redis缓存监控实现
*/
@Component
public class RedisCacheMetrics implements CacheMetrics {
private final RedisTemplate<String, Object> redisTemplate;
private final MeterRegistry meterRegistry;
// 构造函数注入依赖...
private final Counter cacheHitCounter = meterRegistry.counter("cache.hit.count");
private final Counter cacheMissCounter = meterRegistry.counter("cache.miss.count");
private final Counter cacheUpdateSuccessCounter = meterRegistry.counter("cache.update.success.count");
private final Counter cacheUpdateFailCounter = meterRegistry.counter("cache.update.fail.count");
private final Timer cacheResponseTimer = meterRegistry.timer("cache.response.time");
@Override
public double getCacheHitRate() {
double hitCount = cacheHitCounter.count();
double missCount = cacheMissCounter.count();
if (hitCount + missCount == 0) {
return 0;
}
return hitCount / (hitCount + missCount);
}
@Override
public double getCachePenetrationRate() {
// 实现缓存穿透率计算逻辑
}
@Override
public double getAverageResponseTime() {
return cacheResponseTimer.mean(TimeUnit.MILLISECONDS);
}
@Override
public double getCacheUpdateSuccessRate() {
double successCount = cacheUpdateSuccessCounter.count();
double failCount = cacheUpdateFailCounter.count();
if (successCount + failCount == 0) {
return 0;
}
return successCount / (successCount + failCount);
}
@Override
public double getCacheCapacityUsage() {
// 获取Redis内存使用情况
Properties info = redisTemplate.execute(RedisConnection::info);
long usedMemory = Long.parseLong(info.getProperty("used_memory"));
long maxMemory = Long.parseLong(info.getProperty("maxmemory"));
return maxMemory > 0 ? (double) usedMemory / maxMemory : 0;
}
}
7. 企业级缓存最佳实践总结
7.1 缓存策略选择指南
选择缓存策略时应考虑以下因素:
-
业务一致性要求:金融交易、支付等核心业务需优先保证一致性;非核心的统计分析业务可适当降低一致性要求。
-
读写比例:读多写少场景(Cache-Aside);写多读少场景需谨慎使用缓存。
-
数据更新频率:高频更新数据(如实时库存)不适合缓存;低频更新数据(如字典配置)适合缓存。
-
数据量级:大数据量单条记录(如商品详情)适合缓存;小数据量集合(如下拉列表)可整体缓存。
7.2 缓存设计 checklist
实施缓存方案前,建议使用以下checklist进行全面检查:
- 缓存key设计是否合理,是否包含业务标识
- 是否设置了合理的过期时间
- 是否处理了缓存穿透问题
- 是否处理了缓存击穿问题
- 是否处理了缓存雪崩问题
- 是否有缓存一致性保障机制
- 是否有缓存监控和告警机制
- 是否考虑了缓存性能和容量规划
- 是否有缓存降级和容灾方案
- 缓存更新策略是否合理
7.3 缓存优化路线图
8. 总结与展望
缓存一致性是分布式系统中的经典难题,没有放之四海而皆准的完美解决方案。pig平台通过Cache-Aside策略为主体,结合Spring Cache注解和Redis实现了高效可靠的缓存一致性方案,在性能与一致性之间取得了良好平衡。
未来,随着云原生技术的发展,缓存一致性方案将向以下方向演进:
-
智能化:基于AI算法自动学习数据访问模式,动态调整缓存策略。
-
服务化:将缓存管理抽象为独立服务,统一处理缓存一致性问题。
-
云原生:结合Service Mesh等技术,在服务网格层实现透明缓存。
-
强一致性:新的分布式缓存协议(如Redis Cluster)提供更强的一致性保证。
希望本文介绍的pig平台缓存一致性方案能为你的项目提供借鉴,在实际应用中,还需根据具体业务场景灵活调整,找到最适合的缓存策略。
如果本文对你有帮助,请点赞、收藏、关注三连,你的支持是我们持续输出高质量技术文章的动力!
下期预告:《分布式事务解决方案:从理论到实践》,深入探讨微服务架构下的事务一致性问题。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



