簡介
公司有這麼個需求,需用統計店鋪某個時間段(按自然天計算,不超過24小時)類的訂單數量。因爲這些統計數據不用持久化,考慮到性能問題,準備用Redis做統計。
- 設計思路:用Reids的一個有序集合實現。店鋪Id作爲有序集合key,訂單ID作爲有序集合member,插入到Redis時間戳作爲有序集合的score。增加的時候用zadd(cacheKey, System.currentTimeMillis(), orderId),統計的時候用zcount(cacheKey, beginTimestamp, endTimestamp)統計出某個時間段的訂單數量。
- 思考:不能只是想着插入Redis,還必須想着怎麼清理老的數據,也就是清理截止到昨天晚上23:59:59的老數據。自然的想法就是每次生成到昨天的時間戳,然後每次插入的時候清理以前的老數據。
- 問題:
- 1.每次生成昨天的時間戳即使不是個耗時操作,也是沒有必要的,因爲一天只需要生成一次就夠了,其它生成都是浪費的。那怎麼保證新開始一天重新生成昨天的時間戳呢?
- 2.每次插入的時候清理也是沒必要的,只要每天清理一次就行了,因爲我們是按自然天保存訂單數量的。怎麼一天清理一次呢?
- 解決:
總的方案是用guava的緩存,實現類似定時的功能。利用緩存key不存在自動加載;以及Guava的緩存移除觸發器,清理Redis中的老數據。
- 問題1:根據我們自身業務情況,沒有必要及時處理清理老的數據,只要保證Redis內存不爆掉就行了。晚幾個小時甚至晚一天清理一般也不會出問題。所以第一次生成昨天的時間戳可以在本地緩存起來,後續需要的話直接從本地緩存獲取就行了,這樣沒有必要每次都生成這個時間戳,等第二天重新再生成。
- 問題2:更新訂單數量的時候,將店鋪ID和最近清理的時間戳插入到guava緩存中,插入之前先清理老數據;當guava的緩存中的key被因爲過期被清理的時候,觸發監聽器,再次清理老數據。
guava使用demo
public static void main(String[] args) throws ExecutionException, InterruptedException{
//緩存接口這裏是LoadingCache,LoadingCache在緩存項不存在時可以自動加載緩存
LoadingCache<Integer,Student> studentCache
//CacheBuilder的構造函數是私有的,只能通過其靜態方法newBuilder()來獲得CacheBuilder的實例
= CacheBuilder.newBuilder()
//設置併發級別爲8,併發級別是指可以同時寫緩存的線程數
.concurrencyLevel(8)
//設置寫緩存後8秒鐘過期
.expireAfterWrite(8, TimeUnit.SECONDS)
//設置緩存容器的初始容量爲10
.initialCapacity(10)
//設置緩存最大容量爲100,超過100之後就會按照LRU最近雖少使用算法來移除緩存項
.maximumSize(100)
//設置要統計緩存的命中率
.recordStats()
//設置緩存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
//build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實現自動加載緩存
.build(
new CacheLoader<Integer, Student>() {
@Override
public Student load(Integer key) throws Exception {
System.out.println("load student " + key);
Student student = new Student();
student.setId(key);
student.setName("name " + key);
return student;
}
}
);
源碼實現
public class RedisUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
private RedisUtil() {
}
private static final String YESTERDAY = "yesterday";
private static final RedisExtraService redisExtraService = SpringContext.getBean(RedisExtraService.class);
//緩存昨天最後時刻的時間戳,一天後會更新這個時間戳
private static final LoadingCache<String, Long> timeCache = CacheBuilder.newBuilder()
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, Long>() {
@Override
public Long load(String key) throws Exception {
return DateUtil.getYesterdayEndTime();
}
});
//緩存的key回收時會觸發這個監聽器
private static final RemovalListener<String, Long> removalListener = new RemovalListener<String, Long>() {
@Override
public void onRemoval(RemovalNotification<String, Long> notification) {
String key = notification.getKey();
Long recentNeedRemoveTime = notification.getValue();
//不等於時才清除老數據,因爲等於情況說明創建key的時候已經清理了老數據
if (!recentNeedRemoveTime.equals(getRecentNeedRemoveTime()))
redisExtraService.zremrangeByScore(key, 0, recentNeedRemoveTime);
}
};
//緩存待清理的redis中的key
private static final Cache<String, Long> entityCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.DAYS)
.removalListener(removalListener)
.build();
public static void updateOrderCount(final String entityId, final String orderId) {
checkArgument(entityId != null && orderId != null);
try {
entityCache.get(entityId, new Callable<Long>() {
@Override
public Long call() throws Exception {
//創建key的時候已經清理老數據,並將這時間戳記錄下來,回收key的時候需要這個時間戳
Long recentNeedRemoveTime = getRecentNeedRemoveTime();
redisExtraService.zremrangeByScore(entityId, 0, recentNeedRemoveTime);
return recentNeedRemoveTime;
}
});
} catch (ExecutionException e) {
logger.error("從entityCache清理店鋪昨天的訂單數量失敗, entityId: {}", entityId);
//再次嘗試清理
redisExtraService.zremrangeByScore(entityId, 0, getRecentNeedRemoveTime());
}
String cacheKey = getRedisKey(entityId);
Jedis jedis = redisExtraService.getResource();
// long yesterdayEndTime = DateUtil.getYesterdayEndTime();
// long yesterdayEndTime;
// try {
// yesterdayEndTime = timeCache.get(YESTERDAY);
// } catch (ExecutionException e) {
// logger.error("從timeCache獲取yesterdayEndTime失敗");
// yesterdayEndTime = DateUtil.getYesterdayEndTime();
// }
try {
Pipeline pipeline = jedis.pipelined();
// pipeline.zremrangeByScore(cacheKey, 0, yesterdayEndTime);
pipeline.zadd(cacheKey, System.currentTimeMillis(), orderId);
pipeline.expire(cacheKey, OrderCacheConstant.EXPIRE_DAY);
pipeline.sync();
} finally {
redisExtraService.returnResource(jedis);
}
}
public static Long getOrderCount(String entityId, long beginTimestamp, long endTimestamp) {
checkArgument(entityId != null);
String cacheKey = getRedisKey(entityId);
return redisExtraService.zcount(cacheKey, beginTimestamp, endTimestamp);
}
protected static String getRedisKey(String entityId) {
return OrderCacheConstant.KEY_ORDER_COUNT + entityId;
}
/**
* 獲取最近需要刪除的時間,一般是昨天最後的時間
*
* @return
*/
private static Long getRecentNeedRemoveTime() {
Long yesterdayEndTime;
try {
yesterdayEndTime = timeCache.get(YESTERDAY);
} catch (ExecutionException e) {
logger.error("從timeCache獲取yesterdayEndTime失敗");
yesterdayEndTime = DateUtil.getYesterdayEndTime();
}
return yesterdayEndTime;
}
}