分布式服务之分布式缓存(layering-cache)

很多高性能高并发的优化最有效果的优化就是做缓存,

缓存又分本地缓存和分布式缓存,分布式缓存大多数用redis,

但是高并发下的redis有时候网络消耗多的时候也扛不住,

于是redis又可以做分布式redis,增加redis的服务器节点和配置,

但是这个成本也比较高,其实完全可以用本地缓存+redis缓存结合的方式,

保证高并发下的响应速度。

下面是git上一个本地缓存(一级缓存)+redis(二级缓存)的开发模式

git地址:https://github.com/xiaolyuh/layering-cache

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率(Caffeine 缓存详解)。优点数据就在应用内存所以速度快。缺点受应用内存的限制,所以容量有限;没有持久化,重启服务后缓存数据会丢失;在分布式环境下缓存数据数据无法同步;
  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。优点支持多种数据类型,扩容方便;有持久化,重启应用服务器缓存数据不会丢失;他是一个集中式缓存,不存在在应用服务器之间同步数据的问题。缺点每次都需要访问redis存在IO浪费的情况。

我们可以发现Caffeine和Redis的优缺点正好相反,所以他们可以有效的互补

数据的读取流程:

数据读取流程.jpg

数据的删除流程

数据删除流程.jpg

缓存的同步更新:

基于redis pub/sub 实现一级缓存的更新同步。主要原因有两点:

  1. 使用缓存本来就允许脏读,所以有一定的延迟是允许的 。
  2. redis本身是一个高可用的数据库,并且删除动作不是一个非常频繁的动作所以使用redis原生的发布订阅在性能上是没有问题的

演示步骤:

1,Pom文件引入一些包,关键的包(首先保证你的代码增删改查正常),注意springboot版本最好2.1+

      

 <!-- 分布式缓存 开始 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaolyuh</groupId>
            <artifactId>layering-cache-starter</artifactId>
            <version>2.0.7</version>
        </dependency>
        <!-- 分布式缓存 结束 -->

2,配置文件,自己配jdbc和redis配置,如果需要监控缓存情况可加上如下:

spring:
  layering-cache:
    layering-cache-servlet-enabled: true
    stats: true
    url-pattern: /layering-cache/*
    login-username: admin
    login-password: admin
    enable-update: true
    allow: 127.0.0.1

3,在启动类或者是config配置类加上注解  @EnableCaching 启动缓存

4,在需要缓存的方法前加注解

@Override
    @CacheEvict(value = "good", key = "#jsonObject['id']")
    public JSONObject update(JSONObject jsonObject){
        shrekGoodsDao.update(jsonObject);
        return CommonUtil.successJson();
    }

@Override
    @Cacheable(value = "good", key = "#jsonObject['id']", depict = "商品信息缓存",
            firstCache = @FirstCache(expireTime = 1),
            secondaryCache = @SecondaryCache(expireTime = 15, preloadTime = 8, forceRefresh = true))
    public JSONObject detail(JSONObject jsonObject) {
        return shrekGoodsDao.detail(jsonObject).get(0);
    }

    @Override
    @Cacheable(value = "goodImg", key = "#jsonObject['id']", depict = "商品图片信息缓存",
            firstCache = @FirstCache(expireTime = 1),
            secondaryCache = @SecondaryCache(expireTime = 15, preloadTime = 8, forceRefresh = true))
    public List<JSONObject> swiper(JSONObject jsonObject) {
        return shrekGoodsDao.swiper(jsonObject);
    }

  我这里缓存的规则是以第一个为例   缓存表别名 good,key取json的id值,一级缓存失效时间为1,单位分钟,

二级缓存失效时间 15,主动刷新时间 8,单位小时。

测试结果:

    当第一次查询:redis和本地Caffeine都无缓存,将从数据库查询出并缓存至二级缓存(redis)和一级缓存(Caffeine)

    当第二次查询:日志:

2020-04-07 17:12:17.751 DEBUG 15532 --- [nio-8090-exec-5] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key="1" 获取缓存
2020-04-07 17:12:17.753 DEBUG 15532 --- [nio-8090-exec-5] com.github.xiaolyuh.cache.LayeringCache  : 查询一级缓存。 key=1,返回值是:{"price":200.00,"intro":"这个商品是测试用的商品","chose":[{"col":"红色","size":"100","id":1},{"col":"白色","size":"200","id":2}],"id":1,"title":"商品1"}
2020-04-07 17:12:17.753 DEBUG 15532 --- [nio-8090-exec-5] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key="1" 获取缓存
2020-04-07 17:12:17.754 DEBUG 15532 --- [nio-8090-exec-5] com.github.xiaolyuh.cache.LayeringCache  : 查询一级缓存。 key=1,返回值是:[{"id":1,"imgSrc":"api/images/timg.jpg"},{"id":2,"imgSrc":"api/images/timg.jpg"},{"id":3,"imgSrc":"api/images/timg.jpg"},{"id":7,"imgSrc":"api/images/u=2957957338,3157930748&fm=15&gp=0.jpg"},{"id":8,"imgSrc":"api/images/u=2957957338,3157930748&fm=15&gp=0.jpg"}]

第三次查询:间隔1分钟以后(我前面是指的一级缓存失效时间为1分钟)

2020-04-07 17:14:11.776 DEBUG 15532 --- [nio-8090-exec-6] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key="1" putIfAbsent 缓存,缓存值:{"price":200.00,"intro":"这个商品是测试用的商品","chose":[{"col":"红色","size":"100","id":1},{"col":"白色","size":"200","id":2}],"id":1,"title":"商品1"}
2020-04-07 17:14:11.777 DEBUG 15532 --- [nio-8090-exec-6] com.github.xiaolyuh.cache.LayeringCache  : 查询二级缓存,并将数据放到一级缓存。 key=1,返回值是:{"price":200.00,"intro":"这个商品是测试用的商品","chose":[{"col":"红色","size":"100","id":1},{"col":"白色","size":"200","id":2}],"id":1,"title":"商品1"}
2020-04-07 17:14:11.777 DEBUG 15532 --- [nio-8090-exec-6] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key="1" 获取缓存
2020-04-07 17:14:11.777 DEBUG 15532 --- [nio-8090-exec-6] com.github.xiaolyuh.cache.LayeringCache  : 查询一级缓存。 key=1,返回值是:null
2020-04-07 17:14:11.777 DEBUG 15532 --- [nio-8090-exec-6] c.g.xiaolyuh.cache.redis.RedisCache      : redis缓存 key= goodImg:1 查询redis缓存如果没有命中,从数据库获取数据

第四次更新或者删除:将会根据注解的 value和id清楚缓存,同时更新数据库

2020-04-07 17:25:25.128 DEBUG 18044 --- [nio-8090-exec-7] c.g.xiaolyuh.listener.RedisPublisher     : redis消息发布者向频道【good】发布了【com.github.xiaolyuh.listener.RedisPubSubMessage@3611a348】消息
2020-04-07 17:25:25.128 DEBUG 18044 --- [enerContainer-4] c.g.xiaolyuh.listener.RedisPublisher     : redis消息订阅者接收到频道【good】发布的消息。消息内容:{"cacheName":"good","key":1,"messageType":"EVICT"}
2020-04-07 17:25:25.128 DEBUG 18044 --- [enerContainer-4] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key=1 清除缓存
2020-04-07 17:25:25.128  INFO 18044 --- [enerContainer-4] c.g.xiaolyuh.listener.RedisPublisher     : 删除一级缓存good数据,key=1
2020-04-07 17:25:25.128 DEBUG 18044 --- [enerContainer-4] c.g.x.cache.caffeine.CaffeineCache       : caffeine缓存 key=1 清除缓存
2020-04-07 17:25:25.128 DEBUG 18044 --- [nio-8090-exec-7] c.s.e.dao.mysql.ShrekGoodsDao.update     : ==>  Preparing: UPDATE shrek_goods SET title=?, intro=?, price=?, categoryId=? ,sctionId=?,imgPath=? , delete_status=? WHERE id = ?; 
2020-04-07 17:25:25.128  INFO 18044 --- [enerContainer-4] c.g.xiaolyuh.listener.RedisPublisher     : 删除一级缓存good数据,key=1
2020-04-07 17:25:25.129 DEBUG 18044 --- [nio-8090-exec-7] c.s.e.dao.mysql.ShrekGoodsDao.update     : ==> Parameters: 商品1(String), 这个商品是测试用的商11品(String), 200.0(Double), null, null, null, 1(Integer), 1(Integer)
2020-04-07 17:25:25.130 DEBUG 18044 --- [nio-8090-exec-7] c.s.e.dao.mysql.ShrekGoodsDao.update     : <==    Updates: 1

注解说明

@Cacheable

表示用的方法的结果是可以被缓存的,当该方法被调用时先检查缓存是否命中,如果没有命中再调用被缓存的方法,并将其返回值放到缓存中。

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
depict 空字符串 缓存描述(在缓存统计页面会用到)
ignoreException true 是否忽略在操作缓存中遇到的异常,如反序列化异常
firstCache   一级缓存配置
secondaryCache   二级缓存配置
keyGenerator   key生成器,暂时不支持配置

@FirstCache

一级缓存配置项

名称 默认值 说明
initialCapacity 10 缓存初始Size
maximumSize 5000 缓存最大Size
expireTime 9 缓存有效时间
timeUnit TimeUnit.MINUTES 时间单位,默认分钟
expireMode ExpireMode.WRITE 缓存失效模式,ExpireMode.WRITE:最后一次写入后到期失效,ExpireMode.ACCESS:最后一次访问后到期失效

@SecondaryCache

二级缓存配置项

名称 默认值 说明
expireTime 5 缓存有效时间
preloadTime 1 缓存主动在失效前强制刷新缓存的时间,建议是 expireTime * 0.2
timeUnit TimeUnit.HOURS 时间单位,默认小时
forceRefresh false 是否强制刷新(直接执行被缓存方法)
isAllowNullValue false 是否允许缓存NULL值
magnification 1 非空值和null值之间的时间倍率,默认是1。isAllowNullValue=true才有效

@CachePut

将数据放到缓存中

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
depict 空字符串 缓存描述(在缓存统计页面会用到)
ignoreException true 是否忽略在操作缓存中遇到的异常,如反序列化异常
firstCache   一级缓存配置
secondaryCache   二级缓存配置
keyGenerator   key生成器,暂时不支持配置

@CacheEvict

删除缓存

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
allEntries false 是否删除缓存中所有数据,默认情况下是只删除关联key的缓存数据,当该参数设置成 true 时 key 参数将无效
ignoreException true 是否忽略在操作缓存中遇到的异常,如反序列化异常
keyGenerator   key生成器,暂时不支持配置
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章