通用双缓存服务使用说明

通用双缓存服务使用说明

简介

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);
    }
} 
相关推荐
NUZGNAW4 小时前
Ubuntu 安装redis和nginx
redis·nginx·ubuntu
卓航20 小时前
Redis slowlog使用和实现
redis
波吉爱睡觉20 小时前
Redis反弹Shell
redis·web安全·网络安全
骑着蜗牛闯宇宙21 小时前
Thinkphp8 Redis队列与消息队列Queue
redis·php
是阿建吖!1 天前
【Redis】初识Redis(定义、特征、使用场景)
数据库·redis·缓存
neoooo1 天前
《锁得住,才能活得久》——一篇讲透 Redisson 分布式锁的技术实录
java·spring boot·redis
Villiam_AY1 天前
Redis 缓存机制详解:原理、问题与最佳实践
开发语言·redis·后端
GEM的左耳返2 天前
Java面试全攻略:Spring生态与微服务架构实战
spring boot·redis·spring cloud·微服务·kafka·java面试
程序员勋勋12 天前
Redis的String数据类型底层实现
数据库·redis·缓存
颜颜yan_2 天前
Python面向对象编程详解:从零开始掌握类的声明与使用
开发语言·redis·python