很多高性能高并发的优化最有效果的优化就是做缓存,
缓存又分本地缓存和分布式缓存,分布式缓存大多数用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的优缺点正好相反,所以他们可以有效的互补
数据的读取流程:
数据的删除流程
缓存的同步更新:
基于redis pub/sub 实现一级缓存的更新同步。主要原因有两点:
- 使用缓存本来就允许脏读,所以有一定的延迟是允许的 。
- 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生成器,暂时不支持配置 |