高頻寫入redis場景優化

????????發表於2018-12-07

前言

工作中經常遇到要對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 CacheConfig 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的寫入,還可用於其他場景,具體使用方法可以按照業務場景自己擴充套件。

相關文章