本文首发于Ressmix个人站点:https://www.tpvlog.com
在多级缓存架构中,对于实时性要求比较高的数据,通常会以 Cache Aside模式+内存队列的方式进行缓存,也就是前几章我搭建的库存服务。
但是对于一些实时性要求不高的数据,并不需要通过这么复杂的方式去实现。比如,一些商品基本信息、商品店铺信息等等。这些信息我们通常按照不同的维度保存在分布式缓存中(比如Redis),当这些维度的商品相关信息变化时,通过MQ通知缓存数据生产服务,这样缓存数据生产服务就可以异步的更新这些实时性要求不高的数据了。
本章,我就通过实战来讲解如何实现缓存数据生产服务的本地JVM缓存,整体的架构就是 《系统整体架构》一章中架构图的左侧部分,我稍微修改了下,放大如下:
整个实现流程如下:
缓存数据生产服务会订阅多个kafka topic,每个topic对应一类商品相关服务;
如果一个商品相关服务有数据发生了变更,就会发送一个消息到对应的topic中;
缓存数据生产服务监听到消息后,调用对应源服务的数据接口获取更新的数据;
缓存数据生产服务获取到数据后,将数据在本地JVM缓存中写入一份(本案例采用ehcache实现),同时往Redis分布式缓存中也写入一份。
本章我只实现上图中的商品基本信息服务和店铺服务,其它服务读者可类比自己去实现,不再赘述。本章应用的代码存放在
Gitee:4.缓存数据生产服务(https://gitee.com/ressmix/epay/tree/master/4.%E7%BC%93%E5%AD%98%E6%95%B0%E6%8D%AE%E7%94%9F%E4%BA%A7%E6%9C%8D%E5%8A%A1)的
epay-cache
项目下。
一、应用搭建
缓存数据生产服务,作为一个独立应用(epay-cache),涉及到使用本地JVM缓存和Redis缓存,本节我们先搭建好应用,服务启动在8080端口。
1.1 application.properties
1server.port=8080
2spring.application.name=epay-cache
3
4####### database Config #######
5spring.datasource.driver-class-name=com.mysql.jdbc.Driver
6spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
7spring.datasource.url=jdbc:mysql://192.168.0.108:3306/epay?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
8spring.datasource.username=root
9spring.datasource.password=root
10
11# MyBatis
12mybatis.mapper-locations=classpath:/mybatis/*Mapper.xml
13mapper.identity=MYSQL
14
15# Redis
16spring.redis.cluster.nodes=192.168.0.107:7001,192.168.0.107:7002,192.168.0.109:7003,192.168.0.109:7004,192.168.0.110:7005,192.168.0.110:7006
17spring.redis.cluster.maxTotal=200
18spring.redis.cluster.maxIdle=8
19spring.redis.cluster.minIdle=2
20
21# ehcache
22spring.cache.jcache.config=classpath:ehcache.xml
注意,我采用Ehcache作为JVM缓存的实现,读者也可以采用其它的本地缓存框架,比如Guava。
1.2 Ehcache配置
关于Ehcache,我这里要简单说下:Ehcache是一套开源的本地缓存框架,它有针对JSR-107的标准实现,也就是说我们只要引入JSR-107 api包和ehcache实现包,就可以在代码里按照JSR-107的接口规范编程了,以后即使底层的本地缓存实现换了成了Guava,那我们的代码其实也不需要变动的。
JSR是Java Specification Requests(Java 规范提案)的缩写。2012年10月26日JSR规范委员会发布了JSR 107(JCache API的首个早期草案),JCache规范定义了Java缓存的核心接口和功能,我们可以把它类比成Servlet规范来理解。
首先,我们引入Ehcache的maven依赖:
1<!--Ehcache-->
2<dependency>
3 <groupId>org.ehcache</groupId>
4 <artifactId>ehcache</artifactId>
5 <version>3.8.1</version>
6</dependency>
7<!--JSR-107-->
8<dependency>
9 <groupId>javax.cache</groupId>
10 <artifactId>cache-api</artifactId>
11 <version>1.1.1</version>
12</dependency>
然后,在application.properties
中加上Ehcache整合Spring的配置:
1# ehcache
2spring.cache.jcache.config=classpath:ehcache.xml
ehcache.xml
中主要是ehcache针对缓存策略的配置:
1<?xml version="1.0" encoding="UTF-8"?>
2<eh:config
3 xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
4 xmlns:eh='http://www.ehcache.org/v3'
5 xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.3.xsd">
6
7 <!--缓存策略模板-->
8 <eh:cache-template name="default">
9 <eh:expiry>
10 <eh:none/>
11 </eh:expiry>
12 <eh:resources>
13 <!--堆内存可以放2000个条目,超出部分堆外100MB-->
14 <eh:heap unit="entries">2000</eh:heap>
15 <eh:offheap unit="MB">100</eh:offheap>
16 </eh:resources>
17 </eh:cache-template>
18
19 <!--实际的缓存策略,继承了default缓存模板-->
20 <eh:cache alias="local" uses-template="default">
21 </eh:cache>
22</eh:config>
最后,创建一个Ehcache配置类,加上@EnableCaching
注解,这样就可以通过JSR-107注解来使用缓存了:
1@Configuration
2@EnableCaching
3public class EhcacheConfig {
4}
关于Spring如何整合Ehcache的更多内容,读者可以参考Spring官方文档(https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache-store-configuration-jsr107)。
1.3 启动类
1@SpringBootApplication(scanBasePackages = "com.tpvlog.epay.cache")
2@MapperScan("com.tpvlog.epay.cache.mapper")
3@ServletComponentScan
4public class CacheStarter {
5 public static void main(String[] args) {
6 SpringApplication.run(CacheStarter.class, args);
7 }
8}
二、功能实现
2.1 缓存服务
接着,我们就可以使用缓存了,先定义好商品基本信息和店铺信息的Bean:
1/**
2 * 商品基本信息
3 *
4 * @author ressmix
5 */
6public class ProductInfo {
7 private Long id;
8 private Long productId;
9 private String name;
10 private Double price;
11 private String pictureList;
12 private String specification;
13 private String service;
14 private String color;
15 private String size;
16 private Long shopId;
17 private String modifiedTime;
18
19 // ...
20}
1/**
2 * 店铺信息
3 *
4 * @author ressmix
5 */
6public class ShopInfo {
7 private Long id;
8 private Long shopId;
9 private String name;
10 private Integer level;
11 private Double goodCommentRate;
12 private String modifiedTime;
13
14 // ...
15}
由于需要针对商品基本信息和店铺信息进行本地缓存,我们先看下缓存服务的接口:
1/**
2 * 缓存service接口
3 *
4 * @author resmix
5 */
6public interface CacheService {
7
8 /**
9 * 将商品基本信息保存到本地的ehcache缓存中
10 *
11 * @param productInfo
12 */
13 public void saveProductInfo2LocalCache(ProductInfo productInfo);
14
15 /**
16 * 从本地ehcache缓存中获取商品基本信息
17 *
18 * @param productId
19 * @return
20 */
21 public ProductInfo getProductInfoFromLocalCache(Long productId);
22
23 /**
24 * 将店铺信息保存到本地的ehcache缓存中
25 *
26 * @param shopInfo
27 */
28 public void saveShopInfo2LocalCache(ShopInfo shopInfo);
29
30 /**
31 * 从本地ehcache缓存中获取店铺信息
32 *
33 * @param shopId
34 * @return
35 */
36 public ShopInfo getShopInfoFromLocalCache(Long shopId);
37
38 /**
39 * 将商品基本信息保存到redis中
40 *
41 * @param productInfo
42 */
43 public void saveProductInfo2ReidsCache(ProductInfo productInfo);
44
45 /**
46 * 将店铺信息保存到redis中
47 *
48 * @param shopInfo
49 */
50 public void saveShopInfo2ReidsCache(ShopInfo shopInfo);
51
52 /**
53 * 从redis中获取商品信息
54 *
55 * @param productId
56 */
57 public ProductInfo getProductInfoFromReidsCache(Long productId);
58
59 /**
60 * 从redis中获取店铺信息
61 *
62 * @param shopId
63 */
64 public ShopInfo getShopInfoFromReidsCache(Long shopId);
65}
缓存服务实现如下,通过注解@CachePut
可以将数据更新到JVM缓存,通过注解@Cacheable
可以直接从JVM缓存查询数据:
1@Service("cacheService")
2public class CacheServiceImpl implements CacheService {
3
4 @Autowired
5 private RedisDao redisDao;
6
7 @CachePut(value = "local", key = "'product_info_' + #productInfo.getProductId()")
8 @Override
9 public void saveProductInfo2LocalCache(ProductInfo productInfo) {
10 }
11
12 @Cacheable(value = "local", key = "'product_info_'+ #productId")
13 @Override
14 public ProductInfo getProductInfoFromLocalCache(Long productId) {
15 // 没有缓存则返回null
16 return null;
17 }
18
19 @CachePut(value = "local", key = "'shop_info_'+#shopInfo.getShopId()")
20 @Override
21 public void saveShopInfo2LocalCache(ShopInfo shopInfo) {
22 }
23
24 @Cacheable(value = "local", key = "'shop_info_'+#shopId")
25 @Override
26 public ShopInfo getShopInfoFromLocalCache(Long shopId) {
27 return null;
28 }
29
30 @Override
31 public void saveProductInfo2ReidsCache(ProductInfo productInfo) {
32 String key = "product_info_" + productInfo.getProductId();
33 redisDao.set(key, JSON.toJSONString(productInfo));
34 }
35
36 @Override
37 public void saveShopInfo2ReidsCache(ShopInfo shopInfo) {
38 String key = "shop_info_" + shopInfo.getShopId();
39 redisDao.set(key, JSON.toJSONString(shopInfo));
40 }
41
42 @Override
43 public ProductInfo getProductInfoFromReidsCache(Long productId) {
44 String key = "product_info_" + productId;
45 String data = redisDao.get(key);
46 if (StringUtils.isBlank(data)) {
47 return null;
48 }
49 return JSON.parseObject(data, ProductInfo.class);
50 }
51
52 @Override
53 public ShopInfo getShopInfoFromReidsCache(Long shopId) {
54 String key = "shop_info_" + shopId;
55 String data = redisDao.get(key);
56 if (StringUtils.isBlank(data)) {
57 return null;
58 }
59 return JSON.parseObject(data, ShopInfo.class);
60 }
61}
2.2 Controller
最后,创建一个Controller,用于对外暴露http接口,提供查找商品基本信息和店铺信息的功能:
1@Controller
2public class CacheController {
3
4 @Autowired
5 private CacheService cacheService;
6
7 private static final Logger LOG = LoggerFactory.getLogger(CacheController.class);
8
9 /**
10 * 获取商品基本信息
11 *
12 * @param productId
13 * @return
14 */
15 @RequestMapping("/getProductInfo")
16 @ResponseBody
17 public ProductInfo getProductInfo(@RequestParam("productId") Long productId) {
18 ProductInfo productInfo = null;
19
20 // 1.从Redis查找商品基本信息
21 productInfo = cacheService.getProductInfoFromReidsCache(productId);
22 LOG.info("=================从redis中获取缓存,商品信息=" + productInfo);
23
24 if (productInfo == null) {
25 // 2.从本地JVM缓存查找
26 productInfo = cacheService.getProductInfoFromLocalCache(productId);
27 LOG.info("=================从ehcache中获取缓存,商品信息=" + productInfo);
28 }
29
30 if (productInfo == null) {
31 // TODO: 从源服务请求数据,重建缓存,这里先不讲
32 }
33
34 return productInfo;
35 }
36
37 /**
38 * 获取店铺信息
39 *
40 * @param shopId
41 * @return
42 */
43 @RequestMapping("/getShopInfo")
44 @ResponseBody
45 public ShopInfo getShopInfo(@RequestParam("shopId") Long shopId) {
46 ShopInfo shopInfo = null;
47
48 // 1.从Redis查找店铺信息
49 shopInfo = cacheService.getShopInfoFromReidsCache(shopId);
50 LOG.info("=================从redis中获取缓存,店铺信息=" + shopInfo);
51
52 if (shopInfo == null) {
53 // 2.从本地JVM缓存查找
54 shopInfo = cacheService.getShopInfoFromLocalCache(shopId);
55 LOG.info("=================从ehcache中获取缓存,店铺信息=" + shopInfo);
56 }
57
58 if (shopInfo == null) {
59 // TODO: 从源服务请求数据,重建缓存,这里先不讲
60 }
61
62 return shopInfo;
63 }
64}
这里要注意下,我们是先从Redis查找数据,查找不到再从本地JVM缓存去找,如果JVM缓存中也没有,就会调用源服务提供的数据接口查找。回源接口调用的逻辑我标注了TODO
,这块内容涉及缓存重建问题,我后面将专门讲解。
三、总结
本章,我搭建好了缓存数据生产服务——epay-cache,并介绍了ehcache的使用。下一章节,我将实现Kafka客户端功能,订阅数据变更的通知。