暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

分布式实战:缓存数据生产服务之JVM缓存

TPVLOG 2021-06-21
1074

本文首发于Ressmix个人站点:https://www.tpvlog.com

在多级缓存架构中,对于实时性要求比较高的数据,通常会以 Cache Aside模式+内存队列的方式进行缓存,也就是前几章我搭建的库存服务。

但是对于一些实时性要求不高的数据,并不需要通过这么复杂的方式去实现。比如,一些商品基本信息、商品店铺信息等等。这些信息我们通常按照不同的维度保存在分布式缓存中(比如Redis),当这些维度的商品相关信息变化时,通过MQ通知缓存数据生产服务,这样缓存数据生产服务就可以异步的更新这些实时性要求不高的数据了。

本章,我就通过实战来讲解如何实现缓存数据生产服务的本地JVM缓存,整体的架构就是 《系统整体架构》一章中架构图的左侧部分,我稍微修改了下,放大如下:

整个实现流程如下:

  1. 缓存数据生产服务会订阅多个kafka topic,每个topic对应一类商品相关服务;

  2. 如果一个商品相关服务有数据发生了变更,就会发送一个消息到对应的topic中;

  3. 缓存数据生产服务监听到消息后,调用对应源服务的数据接口获取更新的数据;

  4. 缓存数据生产服务获取到数据后,将数据在本地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客户端功能,订阅数据变更的通知。


数据库
文章转载自 TPVLOG,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论

PHP网站源码永湖网站排名优化坑梓百搜词包民治网站优化推广宝安网站优化按天扣费罗湖网站排名优化南澳百搜标王大浪网站搜索优化荷坳网站改版坑梓网站seo优化吉祥外贸网站建设坪山网站开发南山SEO按天扣费横岗外贸网站制作南山seo网站推广双龙企业网站设计南联企业网站设计龙岗网站定制龙岗企业网站改版同乐关键词排名包年推广永湖网站排名优化坪地网站排名优化吉祥网站建设设计松岗百姓网标王推广南山网页制作大鹏高端网站设计坂田网站制作设计吉祥网络推广松岗网站改版龙岗SEO按天收费塘坑模板制作歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

PHP网站源码 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化