很多高性能高併發的優化最有效果的優化就是做緩存,
緩存又分本地緩存和分佈式緩存,分佈式緩存大多數用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生成器,暫時不支持配置 |