通用双缓存服务使用说明

通用双缓存服务使用说明

简介

DualCacheService是一个通用的缓存服务实现,采用短TTL+长TTL的双缓存策略,结合本地缓存、逻辑过期和异步刷新机制,实现高性能且防止缓存击穿的缓存方案。

特性

  • 三级缓存:本地缓存(Caffeine) -> Redis短TTL缓存 -> Redis长TTL缓存(带逻辑过期)
  • 防止缓存击穿:使用Redisson分布式锁
  • 防止缓存穿透:使用空值缓存
  • 防止缓存雪崩:使用不同的TTL和随机延迟
  • 异步刷新:逻辑过期后异步刷新缓存,提高用户体验
  • 错误重试:加入重试机制提高系统容错性
  • 降级策略:缓存操作异常时直接查询数据库
  • 批量预热:支持批量缓存预热,降低系统启动压力
  • 通用性:支持任意类型的数据缓存

使用方法

1. 注入DualCacheService

在你的服务中注入DualCacheService

java 复制代码
@Autowired
private DualCacheService<ID, T> dualCacheService;

其中ID是数据的ID类型(如LongString等),T是要缓存的数据类型。

2. 使用getById方法获取数据

java 复制代码
public T getDataById(ID id) {
    // keyPrefix是缓存键的前缀,用于区分不同的业务数据
    // dbFallback是查询数据库的回调函数
    return dualCacheService.getById(id, "yourKeyPrefix", this::queryFromDatabase);
}

// 数据库查询方法
private T queryFromDatabase(ID id) {
    return repository.findById(id);
}

3. 更新缓存

当新增或更新数据时,使用setCache方法更新缓存:

java 复制代码
public T saveData(T data) {
    // 保存到数据库
    T savedData = repository.save(data);
    // 更新缓存
    dualCacheService.setCache(savedData.getId(), savedData, "yourKeyPrefix");
    return savedData;
}

4. 删除缓存

当删除数据时,使用deleteCache方法删除缓存:

java 复制代码
public void deleteData(ID id) {
    // 从数据库删除
    repository.deleteById(id);
    // 删除缓存
    dualCacheService.deleteCache(id, "yourKeyPrefix");
}

5. 缓存预热

系统启动或定时任务时,可以使用批量预热缓存:

java 复制代码
// 批量预热缓存
public void preloadAllData() {
    // 获取所有需要预热的ID列表
    List<ID> allIds = getAllIds(); 
    
    // 批量预热
    dualCacheService.preloadCache(allIds, "yourKeyPrefix", this::batchQueryFromDatabase);
}

// 批量查询数据库
private List<T> batchQueryFromDatabase(List<ID> ids) {
    return repository.findAllByIdIn(ids);
}

处理空值

DualCacheService会自动处理数据库返回的null值:

  1. 当查询数据库返回null时,会在缓存中设置一个空值标记("NULL")
  2. 空值有较短的过期时间(默认2分钟),避免长时间缓存无效数据
  3. 当查询到空值标记时,会直接返回null,不会再查询数据库
java 复制代码
// 设置空值的例子
dualCacheService.setCache(id, null, "yourKeyPrefix");

适用场景

  • 读多写少的高并发场景
  • 对数据一致性要求不是特别高的场景
  • 需要防止缓存击穿和雪崩的场景
  • 需要防止缓存穿透的场景

参数说明

  • SHORT_TTL:短TTL缓存的过期时间,默认5分钟
  • LONG_TTL:长TTL缓存的过期时间,默认12小时
  • LOGICAL_EXPIRE:逻辑过期时间,默认10分钟
  • NULL_VALUE_TTL:空值缓存过期时间,默认2分钟
  • MAX_RETRY_TIMES:最大重试次数,默认3次
  • RETRY_WAIT_TIME:重试等待时间,默认100毫秒

错误处理

DualCacheService内置了以下错误处理机制:

  1. 重试机制:获取锁失败时会进行重试,最多重试3次
  2. 降级策略:当缓存操作发生异常时,会尝试直接查询数据库作为降级措施
  3. 异常日志:详细记录各种异常情况,便于排查问题

案例

参考DualCacheProductServiceImpl类中的实现方式。

java 复制代码
package com.example.cache.service.cache;

import com.example.cache.model.CacheData;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * 通用双缓存服务
 * 采用短TTL+长TTL的双缓存策略,结合逻辑过期和异步刷新,实现无锁的缓存防护机制
 * @param <ID> ID类型
 * @param <T> 缓存数据类型
 */
@Slf4j
@Component
public class DualCacheService<ID, T> {

    // 短时间缓存前缀,物理过期TTL短
    private static final String SHORT_KEY_PREFIX = "short:";
    // 长时间缓存前缀,物理TTL长,但有逻辑过期时间
    private static final String LONG_KEY_PREFIX = "long:";
    // 分布式锁前缀
    private static final String LOCK_KEY_PREFIX = "dual:lock:";
    // 空值缓存标记
    private static final String NULL_VALUE_KEY = "NULL";
    
    // 短TTL缓存物理过期时间(秒)
    private static final long SHORT_TTL = 60 * 5; // 5分钟
    // 长TTL缓存物理过期时间(秒)
    private static final long LONG_TTL = 60 * 60 * 12; // 12小时
    // 逻辑过期时间(秒)
    private static final long LOGICAL_EXPIRE = 60 * 10; // 10分钟
    // 空值缓存过期时间(秒)
    private static final long NULL_VALUE_TTL = 60 * 2; // 2分钟
    // 锁等待时间(秒)
    private static final int LOCK_WAIT_TIME = 10;
    // 最大重试次数
    private static final int MAX_RETRY_TIMES = 3;
    // 重试等待时间(毫秒)
    private static final int RETRY_WAIT_TIME = 100;
    
    // 刷新随机延迟最大值(毫秒),防止同时刷新
    private static final int REFRESH_DELAY_MAX = 1000;
    
    // 本地缓存,作为第一级缓存
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(2, TimeUnit.MINUTES)
            .build();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 根据ID获取数据,使用双缓存策略
     * @param id 对象ID
     * @param keyPrefix 缓存键前缀
     * @param dbFallback 数据库查询回调函数
     * @return 查询到的对象
     */
    public T getById(ID id, String keyPrefix, Function<ID, T> dbFallback) {
        if (id == null) {
            return null;
        }
        
        String cacheKeyPrefix = keyPrefix + ":";
        String shortKey = SHORT_KEY_PREFIX + cacheKeyPrefix + id;
        String longKey = LONG_KEY_PREFIX + cacheKeyPrefix + id;
        String lockKey = LOCK_KEY_PREFIX + cacheKeyPrefix + id;
        
        // 1. 查询本地缓存
        Object localCacheResult = localCache.getIfPresent(shortKey);
        if (localCacheResult != null) {
            log.info("【双缓存】本地缓存命中,key: {}", shortKey);
            // 如果是空值标记,返回null
            if (NULL_VALUE_KEY.equals(localCacheResult)) {
                return null;
            }
            return (T) localCacheResult;
        }
        
        try {
            // 2. 查询Redis短TTL缓存
            Object shortTTLResult = redisTemplate.opsForValue().get(shortKey);
            if (shortTTLResult != null) {
                log.info("【双缓存】短TTL缓存命中,key: {}", shortKey);
                // 如果是空值标记,返回null
                if (NULL_VALUE_KEY.equals(shortTTLResult)) {
                    // 更新本地缓存
                    localCache.put(shortKey, NULL_VALUE_KEY);
                    return null;
                }
                T data = (T) shortTTLResult;
                // 更新本地缓存
                localCache.put(shortKey, data);
                return data;
            }
            
            // 3. 查询Redis长TTL缓存
            Object longTTLResult = redisTemplate.opsForValue().get(longKey);
            if (longTTLResult != null) {
                // 长TTL缓存命中
                CacheData<T> cacheData = (CacheData<T>) longTTLResult;
                T data = cacheData.getData();
                if (cacheData.isExpired()) {
                    // 逻辑过期,返回旧数据,并异步刷新缓存
                    log.info("【双缓存】长TTL缓存命中但逻辑过期,返回旧数据并异步刷新,key: {}", longKey);
                    asyncRefreshCache(id, cacheKeyPrefix, dbFallback);
                } else {
                    log.info("【双缓存】长TTL缓存命中且未过期,key: {}", longKey);
                }
                // 无论是否过期,先将长TTL缓存的数据写入短TTL缓存
                redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
                // 更新本地缓存
                localCache.put(shortKey, data);
                return data;
            }
            
            // 4. 缓存全部未命中,使用Redisson分布式锁防止并发查询数据库
            return queryWithLock(id, keyPrefix, dbFallback, shortKey, longKey, lockKey);
        } catch (Exception e) {
            log.error("【双缓存】获取数据异常,id: {}, 尝试走降级策略", id, e);
            // 降级策略:尝试直接查询数据库
            try {
                return dbFallback.apply(id);
            } catch (Exception ex) {
                log.error("【双缓存】降级查询数据库也失败,id: {}", id, ex);
                throw new RuntimeException("获取数据失败", e);
            }
        }
    }

    /**
     * 使用分布式锁查询并更新缓存
     */
    private T queryWithLock(ID id, String keyPrefix, Function<ID, T> dbFallback, 
                          String shortKey, String longKey, String lockKey) {
        log.info("【双缓存】缓存全部未命中,尝试获取分布式锁,key: {}", lockKey);
        RLock lock = redissonClient.getLock(lockKey);
        
        // 使用循环代替递归,避免栈溢出风险
        int retryTimes = 0;
        while (retryTimes < MAX_RETRY_TIMES) {
            try {
                // 尝试获取锁,最多等待10秒,锁自动释放时间为30秒
                boolean locked = lock.tryLock(LOCK_WAIT_TIME, 30, TimeUnit.SECONDS);
                if (!locked) {
                    // 获取锁失败,说明有其他线程正在更新缓存,等待一会再查Redis
                    log.info("【双缓存】获取分布式锁失败,等待{}ms后重试,key: {}, 重试次数: {}", 
                            RETRY_WAIT_TIME, lockKey, retryTimes + 1);
                    try {
                        TimeUnit.MILLISECONDS.sleep(RETRY_WAIT_TIME);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    
                    // 重新查询Redis,可能其他线程已经更新了缓存
                    // 检查短TTL缓存
                    Object retryShortResult = redisTemplate.opsForValue().get(shortKey);
                    if (retryShortResult != null) {
                        log.info("【双缓存】重试查询短TTL缓存命中,key: {}", shortKey);
                        // 如果是空值标记,返回null
                        if (NULL_VALUE_KEY.equals(retryShortResult)) {
                            localCache.put(shortKey, NULL_VALUE_KEY);
                            return null;
                        }
                        T data = (T) retryShortResult;
                        localCache.put(shortKey, data);
                        return data;
                    }
                    
                    retryTimes++;
                    continue;
                }
                
                try {
                    // 双重检查,获取锁后再次查询缓存
                    // 检查短TTL缓存
                    Object shortTTLResult = redisTemplate.opsForValue().get(shortKey);
                    if (shortTTLResult != null) {
                        log.info("【双缓存】获取锁后再次检查-短TTL缓存命中,key: {}", shortKey);
                        // 如果是空值标记,返回null
                        if (NULL_VALUE_KEY.equals(shortTTLResult)) {
                            localCache.put(shortKey, NULL_VALUE_KEY);
                            return null;
                        }
                        T data = (T) shortTTLResult;
                        localCache.put(shortKey, data);
                        return data;
                    }
                    
                    // 检查长TTL缓存
                    Object longTTLResult = redisTemplate.opsForValue().get(longKey);
                    if (longTTLResult != null) {
                        CacheData<T> cacheData = (CacheData<T>) longTTLResult;
                        T data = cacheData.getData();
                        if (cacheData.isExpired()) {
                            log.info("【双缓存】获取锁后再次检查-长TTL缓存命中但逻辑过期,key: {}", longKey);
                            // 这里不需要异步刷新,因为接下来会直接查询数据库并更新缓存
                        } else {
                            log.info("【双缓存】获取锁后再次检查-长TTL缓存命中且未过期,key: {}", longKey);
                            // 更新短TTL缓存和本地缓存
                            redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
                            localCache.put(shortKey, data);
                            return data;
                        }
                    }
                    
                    // 5. 查询数据库
                    log.info("【双缓存】获取锁后查询数据库,id: {}", id);
                    T data = dbFallback.apply(id);
                    
                    if (data != null) {
                        // 数据存在,更新缓存
                        // 更新短TTL缓存
                        redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
                        // 更新长TTL缓存
                        CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
                        redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
                        // 更新本地缓存
                        localCache.put(shortKey, data);
                    } else {
                        // 数据不存在,缓存空值
                        log.info("【双缓存】数据库查询结果为空,缓存空值,id: {}", id);
                        redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
                        localCache.put(shortKey, NULL_VALUE_KEY);
                    }
                    return data;
                } finally {
                    // 释放锁
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                        log.info("【双缓存】释放分布式锁,key: {}", lockKey);
                    }
                }
            } catch (InterruptedException e) {
                log.error("【双缓存】获取锁被中断,key: {}", lockKey, e);
                Thread.currentThread().interrupt();
                throw new RuntimeException("获取数据被中断", e);
            } catch (Exception e) {
                log.error("【双缓存】锁操作异常,key: {}, 重试次数: {}", lockKey, retryTimes, e);
                retryTimes++;
                if (retryTimes >= MAX_RETRY_TIMES) {
                    throw e;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(RETRY_WAIT_TIME * retryTimes);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        
        // 所有重试都失败,直接查询数据库作为最后手段
        log.warn("【双缓存】所有重试都失败,直接查询数据库,id: {}", id);
        return dbFallback.apply(id);
    }
    
    /**
     * 异步刷新缓存
     * @param id 对象ID
     * @param keyPrefix 缓存键前缀
     * @param dbFallback 数据库查询回调函数
     */
    @Async
    public void asyncRefreshCache(ID id, String keyPrefix, Function<ID, T> dbFallback) {
        // 随机延迟,避免集中刷新
        try {
            int randomDelay = ThreadLocalRandom.current().nextInt(REFRESH_DELAY_MAX);
            Thread.sleep(randomDelay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("【双缓存】异步刷新缓存,id: {}", id);
        
        // 从数据库查询最新数据
        T data = dbFallback.apply(id);
        String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
        String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
        
        try {
            if (data != null) {
                // 更新短TTL缓存
                redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
                
                // 更新长TTL缓存
                CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
                redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
                
                // 更新本地缓存
                localCache.put(shortKey, data);
                
                log.info("【双缓存】异步刷新缓存完成,id: {}", id);
            } else {
                // 数据库不存在,缓存空值
                log.info("【双缓存】异步刷新缓存,数据不存在,缓存空值,id: {}", id);
                redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
                localCache.put(shortKey, NULL_VALUE_KEY);
                // 删除长TTL缓存
                redisTemplate.delete(longKey);
            }
        } catch (Exception e) {
            log.error("【双缓存】异步刷新缓存异常,id: {}", id, e);
        }
    }
    
    /**
     * 设置缓存
     * @param id 对象ID
     * @param data 对象数据
     * @param keyPrefix 缓存键前缀
     */
    public void setCache(ID id, T data, String keyPrefix) {
        if (id == null) {
            return;
        }
        
        String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
        String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
        
        if (data != null) {
            // 更新短TTL缓存
            redisTemplate.opsForValue().set(shortKey, data, SHORT_TTL, TimeUnit.SECONDS);
            
            // 更新长TTL缓存
            CacheData<T> cacheData = CacheData.create(data, LOGICAL_EXPIRE);
            redisTemplate.opsForValue().set(longKey, cacheData, LONG_TTL, TimeUnit.SECONDS);
            
            // 更新本地缓存
            localCache.put(shortKey, data);
        } else {
            // 缓存空值
            redisTemplate.opsForValue().set(shortKey, NULL_VALUE_KEY, NULL_VALUE_TTL, TimeUnit.SECONDS);
            localCache.put(shortKey, NULL_VALUE_KEY);
            // 删除长TTL缓存
            redisTemplate.delete(longKey);
        }
    }
    
    /**
     * 删除缓存
     * @param id 对象ID
     * @param keyPrefix 缓存键前缀
     */
    public void deleteCache(ID id, String keyPrefix) {
        if (id == null) {
            return;
        }
        
        String shortKey = SHORT_KEY_PREFIX + keyPrefix + ":" + id;
        String longKey = LONG_KEY_PREFIX + keyPrefix + ":" + id;
        
        redisTemplate.delete(shortKey);
        redisTemplate.delete(longKey);
        localCache.invalidate(shortKey);
    }
    
    /**
     * 批量预热缓存
     * @param ids ID列表
     * @param keyPrefix 缓存键前缀
     * @param batchDbFallback 批量查询数据库的回调函数
     */
    public void preloadCache(List<ID> ids, String keyPrefix, Function<List<ID>, List<T>> batchDbFallback) {
        if (ids == null || ids.isEmpty()) {
            return;
        }
        
        log.info("【双缓存】开始批量预热缓存,keyPrefix: {}, 数量: {}", keyPrefix, ids.size());
        try {
            // 批量查询数据库
            List<T> dataList = batchDbFallback.apply(ids);
            
            if (dataList != null && !dataList.isEmpty()) {
                // 这里假设T类型对象有getId()方法,实际使用时可能需要提供一个Function来获取ID
                for (T data : dataList) {
                    // 获取对象的ID,这里简化处理,实际应该通过反射或Function获取
                    ID dataId = getIdFromData(data);
                    if (dataId != null) {
                        setCache(dataId, data, keyPrefix);
                    }
                }
                log.info("【双缓存】批量预热缓存完成,keyPrefix: {}, 实际预热数量: {}", keyPrefix, dataList.size());
            } else {
                log.info("【双缓存】批量预热缓存,没有数据,keyPrefix: {}", keyPrefix);
            }
        } catch (Exception e) {
            log.error("【双缓存】批量预热缓存异常,keyPrefix: {}", keyPrefix, e);
        }
    }
    
    /**
     * 从数据对象中获取ID
     * 这是一个示例方法,实际使用时应该根据具体的数据类型实现
     * 可以考虑使用反射或在使用时提供一个Function
     */
    @SuppressWarnings("unchecked")
    private ID getIdFromData(T data) {
        try {
            // 这里假设数据对象有getId方法
            return (ID) data.getClass().getMethod("getId").invoke(data);
        } catch (Exception e) {
            log.error("【双缓存】从数据对象获取ID失败", e);
            return null;
        }
    }
} 
java 复制代码
package com.example.cache.service.impl;

import com.example.cache.model.CacheData;
import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import com.example.cache.service.ProductService;
import com.example.cache.service.cache.DualCacheService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * 采用双缓存策略的产品服务实现
 * 采用短TTL+长TTL的双缓存策略,结合逻辑过期和异步刷新,实现无锁的缓存防护机制
 */
@Slf4j
@Service("dualCacheProductService")
public class DualCacheProductServiceImpl implements ProductService {

    // 缓存键前缀
    private static final String PRODUCT_KEY_PREFIX = "product";
    // 列表缓存键
    private static final String PRODUCT_LIST_KEY = "dual:product:list";
    // 短TTL缓存物理过期时间(秒)
    private static final long SHORT_TTL = 60 * 5; // 5分钟
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private DualCacheService<Long, Product> dualCacheService;
    
    /**
     * 应用启动时预热缓存
     */
    @PostConstruct
    @Override
    public void preloadCache() {
        log.info("【双缓存】开始预热产品缓存...");
        try {
            List<Product> products = productRepository.findAll();
            
            // 批量缓存所有产品
            for (Product product : products) {
                dualCacheService.setCache(product.getId(), product, PRODUCT_KEY_PREFIX);
            }
            
            // 缓存产品列表
            redisTemplate.opsForValue().set(PRODUCT_LIST_KEY, products, SHORT_TTL, TimeUnit.SECONDS);
            
            log.info("【双缓存】缓存预热完成,共预热{}个产品", products.size());
        } catch (Exception e) {
            log.error("【双缓存】缓存预热失败", e);
        }
    }
    
    /**
     * 根据ID获取产品,使用双缓存策略
     * 当缓存全部未命中查询数据库时,使用Redisson分布式锁防止并发查询
     */
    @Override
    public Product getProductById(Long id) {
        return dualCacheService.getById(id, PRODUCT_KEY_PREFIX, productRepository::findById);
    }
    
    /**
     * 获取所有产品,使用双缓存策略
     */
    @Override
    public List<Product> getAllProducts() {
        // 查询Redis短TTL缓存
        Object result = redisTemplate.opsForValue().get(PRODUCT_LIST_KEY);
        if (result != null) {
            log.info("【双缓存】产品列表缓存命中");
            return (List<Product>) result;
        }
        
        // 缓存未命中,查询数据库
        log.info("【双缓存】产品列表缓存未命中,查询数据库");
        List<Product> products = productRepository.findAll();
        
        // 更新缓存
        redisTemplate.opsForValue().set(PRODUCT_LIST_KEY, products, SHORT_TTL, TimeUnit.SECONDS);
        
        return products;
    }
    
    /**
     * 保存产品并更新缓存
     */
    @Override
    public Product saveProduct(Product product) {
        if (product == null) {
            return null;
        }
        
        // 保存到数据库
        Product savedProduct = productRepository.save(product);
        Long id = savedProduct.getId();
        
        // 更新缓存
        dualCacheService.setCache(id, savedProduct, PRODUCT_KEY_PREFIX);
        
        // 删除列表缓存
        redisTemplate.delete(PRODUCT_LIST_KEY);
        
        return savedProduct;
    }
    
    /**
     * 更新产品并更新缓存
     */
    @Override
    public Product updateProduct(Product product) {
        if (product == null || product.getId() == null) {
            return null;
        }
        
        // 更新数据库
        Product updatedProduct = productRepository.save(product);
        Long id = updatedProduct.getId();
        
        // 更新缓存
        dualCacheService.setCache(id, updatedProduct, PRODUCT_KEY_PREFIX);
        
        // 删除列表缓存
        redisTemplate.delete(PRODUCT_LIST_KEY);
        
        return updatedProduct;
    }
    
    /**
     * 删除产品并更新缓存
     */
    @Override
    public void deleteProduct(Long id) {
        if (id == null) {
            return;
        }
        
        // 从数据库删除
        productRepository.deleteById(id);
        
        // 删除缓存
        dualCacheService.deleteCache(id, PRODUCT_KEY_PREFIX);
        
        // 删除列表缓存
        redisTemplate.delete(PRODUCT_LIST_KEY);
    }
} 
相关推荐
bing_15828 分钟前
Redis 的单线程模型对微服务意味着什么?需要注意哪些潜在瓶颈?
数据库·redis·微服务
小黑蛋学java29 分钟前
Redis-cli常用参数及功能的详细说明
redis
听闻风很好吃2 小时前
Redis高级数据类型解析(二)——Set、Sorted Set与Geo实战指南
数据库·redis·缓存
YGGP2 小时前
【每日八股】复习 Redis Day2:Redis 的持久化(下)
redis
知初~2 小时前
java—11 Redis
redis
云攀登者-望正茂2 小时前
Redis 及其在系统设计中的作用
数据库·redis·缓存
冼紫菜3 小时前
基于Redis实现高并发抢券系统的数据同步方案详解
java·数据库·redis·后端·mysql·缓存·性能优化
搬砖天才、5 小时前
日常记录-redis主从复制(master-slave)+ Sentinel 哨兵(高可用)
数据库·redis·sentinel
ShAn DiAn6 小时前
实时步数统计系统 kafka + spark +redis
大数据·redis·分布式·spark·kafka