缓存架构

京东HotKey架构

代码结构

代码详情
功能点:(如代码有错误,欢迎讨论纠正)
-
多级缓存,先查HotKey缓存,再查Redis,最后才查数据库
-
热点数据重建逻辑使用分布式锁,二次查询
-
更新缓存采用读写锁提升性能
-
利用HotKey系统推送热点Key到应用服务端(HotKey客户端角色)
-
适用读多写少的场景
/**
- 常量
**/
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:";
}
/**
初始化京东HotKey
**/
@Component
public class HotkeyInitializer {@PostConstruct public void initHotkey() { ClientStarter.Builder builder = new ClientStarter.Builder(); // 注意,setAppName很重要,它和dashboard中相关规则是关联的。 ClientStarter starter = builder.setAppName("redis-multi-cache") .setEtcdServer("http://127.0.0.1:2379") .setCaffeineSize(10) .build(); starter.startPipeline(); }
}
/**
-
控制器
**/
@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;//写锁
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()));
JdHotKeyStore.smartSet(Constants.PRODUCT_CACHE + productResult.getId(), 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()));
JdHotKeyStore.smartSet(Constants.PRODUCT_CACHE + productResult.getId(), 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())); JdHotKeyStore.smartSet(productCacheKey, 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) {
//检测是不是热点Key并上报
if (JdHotKeyStore.isHotKey(productCacheKey)) {
Object productObj = JdHotKeyStore.get(productCacheKey);
if (productObj != null) {
//如果是热点Key并且有值就直接返回
return (Product) JdHotKeyStore.get(productCacheKey);
} else {
Product product = getProductFromRedis(productCacheKey);
//按文档如果是热点Key,第一次返回的Value是空的,要主动设置一次到本地缓存
JdHotKeyStore.smartSet(productCacheKey, product);
return product;
}
} else {
return getProductFromRedis(productCacheKey);
}
}private Product getProductFromRedis(String productCacheKey) {
Product product = null;
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); }
}
<?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/>
</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>com.jd.platform.hotkey</groupId> <artifactId>hotkey-client</artifactId> <version>0.0.4-SNAPSHOT</version> </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>
- 常量