背景
使用ORM工具是往往會配合緩存框架實現三級緩存提高查詢效率,spring-cache配合redis是非常常規的實現方案,如未做特殊配置,@CacheEvict(allEntries = true) 的批量驅逐方式,默認使用keys的方式查詢歷史緩存列表而后delete,而keys是阻塞式的命令,cache的dbsize越大,效率越低進而形成redis慢查詢。而使用scan增量式迭代指令,配合自動游標批量掃描緩存匹配的緩存key,是一種非阻塞的方案,一般認為更適合用于大規模數據集的遍歷,因為它不會像 keys 那樣一次性加載所有的鍵,可能在性能上更具優勢。
現狀
某項目線上積累數據緩存80萬個key,使用spring-cache(2.7.x)+redis(6.0)作為緩存框架,redis監控中,頻繁出現keys慢日志
問題
改造springboot-cache配置,將keys替換為scan,貌似是一種替代keys減少慢日志的最優解,那到底能否提高效率呢,或者能否帶來其他改變?
結論
在一定數據量的緩存系統中,如本系統dbsize為80萬左右,使用scan替換keys能顯著減少redis慢日志,但讓redis的cpu驟增。這使得系統應對突發問題時會變得更加脆弱,優化顯得入不敷出。
實踐
更新方法
多數AI的QA或者線上搜索的"替換springboot-cache keys指令為scan"實現方式都是片面的,為什么呢,也許現實證明確實沒有必要這么更新所以答案就更加模糊。當然了,實踐是檢驗真理的唯一標準,我們嘗試一下更新。(spring-boot-starter-data-redis version=2.7.x)
@Slf4j
@Configuration
@EnableCaching
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig extends CachingConfigurerSupport { //其他@Override和@Bean參考網絡或自己實現// ……/*** 配置一個CacheManager才能使用@Cacheable等注解*/@Beanpublic CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {log.info("cacheManager(RedisConnectionFactory redisConnectionFactory):" + redisConnectionFactory);return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)// 全局緩存配置.cacheDefaults(redisCacheConfiguration(20 * 60))// 指定特殊緩存過期時間.withCacheConfiguration("sys:config", redisCacheConfiguration(7 * 60))//指定scan匹配key作為驅逐查詢方式 .cacheWriter(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(5000))).transactionAware().build();}}
重點改動就在于這里的nonLockingRedisCacheWriter,至于scan的size=5000,是一個權衡的考慮,設置10000時,實踐發現庫夠大時scan的速度較慢,可能超過20毫秒;設置過小,游標搜索次數又會明顯增多,所以沒有一個合理的選擇。
主要就是定義這個CacheManager然后必須加入cacheWriter,根本不需要單獨寫Writer復雜實現或者僅寫個Writer實現也不在CacheManager中配置(都沒配置關聯上怎么可能生效)。
驗證更新后的指令執行日志如下(觀察sys:setting::*):
#之前
1745380521.036415 [2 192.1.5.172:57514] GET aodc:to-client
1745380521.044084 [0 192.1.5.165:49754] GET aodc:to-client
1745380521.118468 [0 192.1.5.145:64743] KEYS sys:setting::*
1745380521.122180 [0 192.1.5.145:64743] DEL sys:setting::get-values-by-name:ces_equipment_code_blacklist sys:setting::get-value-by-name:hex_msg_for_equipment_list
1745380521.130366 [0 192.1.5.145:64743] HGET user:online-info \xe9\x99\x88\xe6\xac\xa3\xe6\xac\xa3
1745380521.134822 [0 192.1.5.145:64743] HGET user:online-info \xe9\x99\x88\xe6\xac\xa3\xe6\xac\xa3
1745380521.140906 [2 192.1.5.172:57514] GET aodc:to-client
#之后
1745381648.269224 [2 192.1.5.172:57514] SCAN 462 MATCH ocpp:to-equip:*
1745381648.270222 [2 192.1.5.172:57514] SCAN 158 MATCH ocpp:to-equip:*
1745381648.271283 [2 192.1.5.172:57514] SCAN 830 MATCH ocpp:to-equip:*
1745381648.273144 [0 192.1.5.165:49673] SCAN 0 match ocpp:to-equip:*
1745381648.273730 [2 192.1.5.172:57514] SCAN 897 MATCH ocpp:to-equip:*
1745381648.271348 [0 192.1.5.145:49669] SCAN 0 match sys:setting::* count 10000
1745381648.274556 [0 192.1.5.145:49669] DEL sys:setting::get-values-by-name:evcs_anhui_monitor_operator sys:setting::get-values-by-name:amap_operator sys:setting::get-values-by-name:evcs_hubei_monitor_operator sys:setting::get-value-by-name:hex_msg_for_equipment_list sys:setting::get-values-by-name:evcs_lianlian_monitor_operator sys:setting::get-values-by-name:evcs_hunan_loudi_operator sys:setting::get-values-by-name:evcs_cqgw_monitor_operator sys:setting::get-values-by-name:evcs_jilin_monitor_operator sys:setting::get-value-by-name:ces_order_curve_period_seconds sys:setting::get-values-by-name:evcs_anhuishenran_operator sys:setting::get-value-by-name:evcs_result_verify_sign_switch sys:setting::get-values-by-name:evcs_guizhou_monitor_operator sys:setting::get-values-by-name:ces_equipment_code_blacklist sys:setting::get-value-by-name:cec102_special_format_operators
1745381648.275050 [2 192.1.5.172:57514] SCAN 993 MATCH ocpp:to-equip:*
看到keys已替換為scan并進行緩存清除操作。
?
更新效果
在更新上線后,這一段時間內,確實未再出現明顯多的KEYS慢指令,似乎朝著一個非常完美的方向發展。
但是,千萬不要大意,處理監控慢日志,還需要查看其他資源情況,這里就如“結論”中提到的,發布該更新后CPU竟然突增了,而且居高不下。
這里簡單做個分析:
keys雖然阻塞,每次幾十甚至上百毫秒完成一次key匹配和驅逐,但是流程簡單,對系統實質影響不高,對資源占用影響不大;而使用scan后,是非阻塞的游標輪詢,緩存系統中,總key越多,游標需要輪詢的次數就越多,當scan size越小,可能影響越大,這無形中加大了對資源的占用。
當然,這僅限當前項目,可能基于緩存數量、并發量、驅逐頻率、緩存命中率等因素影響,替換keys為scan優化整體效果權衡表現不佳,更多的是需要自行實踐,尋找最佳的方案,目前推薦的方案是,禁用@CacheEvict(allEntries = true),指定key來驅逐緩存,這要求查詢操作能指定對應key,是業務層的另一種優化方案這里就暫不討論。