缓存架构
代码结构
代码详情
功能点:
- 多级缓存,先查本地缓存,再查Redis,最后才查数据库
- 热点数据重建逻辑使用分布式锁,二次查询
- 更新缓存采用读写锁提升性能
- 采用Redis的发布订阅机制通知所有实例更新本地缓存
- 适用读多写少的场景
/**
* 常量
**/
public class Constants {
public static final String PRODUCT_CACHE = "product:cache:";
public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
public static final String EMPTY_CACHE = "{}";
public static final String LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";
public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";
public static final String NOTIFY_LOCAL_CACHE_UPDATE = "notify_local_cache_update";
}
/**
* 本地缓存
**/
public class LocalCacheHolder {
@Getter
private static Map<String, Product> localCache = new ConcurrentHashMap<>();
}
/**
* 控制器
**/
@RestController
@RequestMapping("/api/product/")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping(value = "/create")
public Product createProduct(@RequestBody Product product) {
return productService.createProduct(product);
}
@PostMapping(value = "/update")
public Product updateProduct(@RequestBody Product product) {
return productService.updateProduct(product);
}
@GetMapping("/get/{productId}")
public Product getProduct(@PathVariable Long productId) {
return productService.getProduct(productId);
}
}
/**
* DAO层
**/
@Repository
public class ProductDao {
public Product createProduct(Product product) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("创建商品成功");
return product;
}
public Product updateProduct(Product product) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("修改商品成功");
return product;
}
public Product getProduct(Long productId) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("查询商品成功");
Product product = new Product();
product.setId(productId);
product.setName("test");
return product;
}
}
/**
* 商品实体
**/
@Data
public class Product implements Serializable {
private Long id; // 商品ID
private String name; // 商品名称
private String category; // 商品分类(如 "电子产品"、"图书")
private BigDecimal price; // 价格(精确到分,使用BigDecimal避免浮点精度问题)
private int stock; // 库存数量
private String description; // 商品描述
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
/**
* 商品服务
**/
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Autowired
private Redisson redisson;
@Autowired
private RTopic localCacheUpdateTopic;
//写锁
public Product createProduct(Product product) {
Product productResult = null;
RReadWriteLock readWriteLock = redisson.getReadWriteLock(Constants.LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
RLock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
productResult = productDao.createProduct(product);
redisson.getBucket(Constants.LOCK_PRODUCT_UPDATE_PREFIX + product.getId()).set(JSON.toJSONString(productResult), Duration.ofSeconds(genProductCacheTimeout()));
LocalCacheHolder.getLocalCache().put(Constants.PRODUCT_CACHE + productResult.getId(), product);
localCacheUpdateTopic.publish(JSON.toJSONString(productResult));
} finally {
writeLock.unlock();
}
return productResult;
}
//写锁
public Product updateProduct(Product product) {
Product productResult = null;
RReadWriteLock readWriteLock = redisson.getReadWriteLock(Constants.LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
RLock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
productResult = productDao.updateProduct(product);
redisson.getBucket(Constants.LOCK_PRODUCT_UPDATE_PREFIX + product.getId()).set(JSON.toJSONString(productResult), Duration.ofSeconds(genProductCacheTimeout()));
LocalCacheHolder.getLocalCache().put(Constants.PRODUCT_CACHE + productResult.getId(), product);
localCacheUpdateTopic.publish(JSON.toJSONString(productResult));
} finally {
writeLock.unlock();
}
return productResult;
}
public Product getProduct(Long productId) {
Product product = null;
String productCacheKey = Constants.PRODUCT_CACHE + productId;
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
//热点缓存重建,加分布式锁,第一个请求写缓存成功后,做二次读取缓存操作,其余请求都可以走Redis
RLock hotCacheLock = redisson.getLock(Constants.LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
try {
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
RReadWriteLock readWriteLock = redisson.getReadWriteLock(Constants.LOCK_PRODUCT_UPDATE_PREFIX + productId);
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
product = productDao.getProduct(productId);
if (product != null) {
redisson.getBucket(productCacheKey).set(JSON.toJSONString(product),Duration.ofSeconds(genProductCacheTimeout()));
LocalCacheHolder.getLocalCache().put(productCacheKey, product);
localCacheUpdateTopic.publish(JSON.toJSONString(product));
} else {
redisson.getBucket(productCacheKey).set(Constants.EMPTY_CACHE, Duration.ofSeconds(genEmptyCacheTimeout()));
}
} finally {
rLock.unlock();
}
} finally {
hotCacheLock.unlock();
}
return product;
}
//随机超时时间,防止缓存击穿(失效)
private Integer genProductCacheTimeout() {
return Constants.PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}
//空数据的超时时间
private Integer genEmptyCacheTimeout() {
return 60 + new Random().nextInt(30);
}
//从缓存获取数据
private Product getProductFromCache(String productCacheKey) {
Product product = LocalCacheHolder.getLocalCache().get(productCacheKey);
if (product != null) {
return product;
}
RBucket<String> bucket = redisson.getBucket(productCacheKey);
String productStr = bucket.get();
if (!StringUtils.isEmpty(productStr)) {
if (Constants.EMPTY_CACHE.equals(productStr)) {
bucket.expire(Duration.ofSeconds(genEmptyCacheTimeout()));
return new Product();
}
product = JSON.parseObject(productStr, Product.class);
bucket.expire(Duration.ofSeconds(genProductCacheTimeout())); //读延期
}
return product;
}
}
@SpringBootApplication
public class RedisMultiCacheApplication {
public static void main(String[] args) {
SpringApplication.run(RedisMultiCacheApplication.class, args);
}
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
//通知其它实例更新本地缓存
@Bean
public RTopic localCacheUpdateTopic(Redisson redisson) {
RTopic topic = redisson.getTopic(Constants.NOTIFY_LOCAL_CACHE_UPDATE);
topic.addListener(String.class, (channel, message) -> {
System.out.println("收到消息:" + channel + ":" + message);
Product product = JSON.parseObject(message, Product.class);
LocalCacheHolder.getLocalCache().put(Constants.LOCK_PRODUCT_UPDATE_PREFIX + product.getId(), product);
});
return topic;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.mutilcache</groupId>
<artifactId>redis-multi-cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-multi-cache</name>
<description>redis-multi-cache</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.48.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>