Redis統計某個時間段訂單數量(用Guava緩存輔助解決)

簡介

公司有這麼個需求,需用統計店鋪某個時間段(按自然天計算,不超過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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章