高頻寫入redis場景優化
工作中經常遇到要對redis進行高頻寫入,但是對於讀取時資料的實時性要求又不高的場景。為了優化效能,決定採用本地快取一部分資料整合後寫入。
依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0-rc2</version> </dependency> 複製程式碼
基礎類
public class BufferCache implements Closeable { // CacheBuilder的建構函式是私有的,只能通過其靜態方法newBuilder()來獲得CacheBuilder的例項 private Cache localCacheData; private static int maxItemSize = 1000; private static String key = "defaultKey"; private static final Object lock = new Object(); public BufferCache(String key, int currencyLevel, int writeExpireTime, int accessExpireTime, int initialCapacity, int maximumSize, int maxItemSize, RemovalListener removalListener) { currencyLevel = currencyLevel < 1 ? 1 : currencyLevel; initialCapacity = initialCapacity < 100 ? 100 : initialCapacity; if (key!=null&&key.isEmpty()) { BufferCache.key = key; } BufferCache.maxItemSize = maxItemSize; localCacheData = CacheBuilder.newBuilder() // 設定併發級別為8,併發級別是指可以同時寫快取的執行緒數 .concurrencyLevel(currencyLevel) // 設定寫快取後expireTime秒鐘過期 .expireAfterWrite(writeExpireTime, TimeUnit.SECONDS) // 設定請求後expireTime秒鐘過期 .expireAfterAccess(accessExpireTime, TimeUnit.SECONDS) // 設定快取容器的初始容量為10 .initialCapacity(initialCapacity) // 設定快取最大容量為Integer.MAX_VALUE,超過Integer.MAX_VALUE之後就會按照LRU最近雖少使用演算法來移除快取項 .maximumSize(maximumSize) // 設定要統計快取的命中率 .recordStats() // 設定快取的移除通知 .removalListener(removalListener) // build方法中可以指定CacheLoader,在快取不存在時通過CacheLoader的實現自動載入快取 .build(); Runtime.getRuntime().addShutdownHook( new Thread(() -> localCacheData.invalidate(key))); } public void addListSync(String key, Object value) { synchronized (lock) { List<Object> gs = (List<Object>) localCacheData.getIfPresent(key); if (gs == null) { gs = new ArrayList<>(); } gs.add(value); localCacheData.put(key, gs); // 如果佇列長度超過設定最大長度則清除key if (gs.size() > maxItemSize) { localCacheData.invalidate(key); } } } public void addListSync(Object value) { addListSync(BufferCache.key, value); } @Override public void close() { localCacheData.invalidate(key); } } 複製程式碼
採用 google 的 cache,利用其監聽事件(詳見 com.google.common.cache.RemovalCause 類)觸發寫入redis操作,addListSync方法中使用 synchronized 進行加鎖,防止高併發場景下List資料錯誤。
新建配置檔案
cache.key=name cache.currencyLevel=1 cache.writeExpireTime=900 cache.accessExpireTime=600 cache.initialCapacity=1 cache.maximumSize=1000 cache.maxItemSize=1000 複製程式碼
針對不同業務場景可以自定義不同的配置引數
業務實現
@Configuration @ConditionalOnResource(resources = "bufferCache.properties") @PropertySource(value = "bufferCache.properties", ignoreResourceNotFound = true) public class GuildCacheConfig implements ApplicationContextAware { private ApplicationContext ctx; @Bean("buffCache") @ConditionalOnProperty(prefix = "cache", value = "currencyLevel") public BufferCache guildBuffCache(@Value("${cache.key}") String key, @Value("${cache.currencyLevel}") int currencyLevel, @Value("${cache.writeExpireTime}") int writeExpireTime, @Value("${cache.accessExpireTime}") int accessExpireTime, @Value("${cache.initialCapacity}") int initialCapacity, @Value("${cache.maximumSize}") int maximumSize, @Value("${cache.maxItemSize}") int maxItemSize) { // 非同步監聽 RemovalListener<String, List<GuildActiveEventEntity>> async = RemovalListeners .asynchronous(new MyRemovalListener(), ExecutorServiceUtil.getExecutorServiceByType( ExecutorServiceUtil.ExecutorServiceType.BACKGROUND)); return new BufferCache(key, currencyLevel, writeExpireTime, accessExpireTime, initialCapacity, maximumSize, maxItemSize, async); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ctx = applicationContext; } // 建立一個監聽器 private class MyRemovalListener implements RemovalListener<String, List<GuildActiveEventEntity>> { @Override public void onRemoval( RemovalNotification<String, List<GuildActiveEventEntity>> notification) { RemovalCause cause = notification.getCause(); // 當超出快取佇列限制大小時或者key過期或者主動清除key時更新資料 if (cause.equals(RemovalCause.SIZE) || cause.equals(RemovalCause.EXPIRED) || cause.equals(RemovalCause.EXPLICIT)) { //根據不同業務場景呼叫不同業務方法進行寫入操作 } } } } 複製程式碼
此類實現 ApplicationContextAware 為了獲取指定業務方法 Bean ,進行解析快取中value模型後進行儲存。 在以上幾個步驟都完成後,只需在業務層聲名
@Autowired private BufferCache buffCache; 複製程式碼
呼叫其addListSync方法即可。
總結
總體思路是使用本地快取去分擔高頻寫的壓力,此方法其實不僅僅適用與redis的寫入,還可用於其他場景,具體使用方法可以按照業務場景自己擴充套件。