使用 Redis 緩存來實現用戶最近瀏覽的商品列表


背景

最近在開發商品瀏覽歷史,由於錯誤選擇了 Redis 的數據結構來進行存儲,導致性能低下。下面我們來分析一下瀏覽歷史需要考慮的點,以及實現上可選的數據結構。

實現思路

首先我們可以確認以下問題:

怎么添加

用戶最近瀏覽的商品,肯定是要在用戶打開商品詳情頁的時候才算瀏覽。

分頁

Redis 中 ListLRANGE 可以指定獲取指定長度的元素,可選。

Redis 中 SortedSet 有序集合,可以通過 ZREVRANGEBYSCORE 來分頁,可選。

map 不支持分頁,直接 pass。

數據重復

如果使用 List 來存儲商品,由於 list 是不支持去重的,pass。

使用map來存儲,可以通過map key的唯一性來保證元素唯一,但是map不支持分頁 pass。

使用 SetSortedSet 來存儲,由於 set 天然支持去重,可選。

排序

如果使用 List 和 Map 來存儲,需要手動指定瀏覽日期(需要精確到秒)的方式,來在內存中進行排序,很不友好。

這里可以選擇上面提到的 SortedSet ,該數據結構有一個 score 值,可以進行排序。

失效時間

瀏覽歷史肯定是存在過期時間的,這一點上選擇 Redis 來設置最大過期時間(最大可以為一個月)是很方便的。

如果使用關系型數據庫,可能還需要定時清理數據庫中的數據。相比於 Redis 的 expire 就麻煩的多。

結論

結合上面的考慮,選擇 SortedSet 是最好的選擇。對去重和排序比較友好,並且使用SortedSet最大的好處是在查詢的時候就可以通過偏移量來進行分頁查詢,而不需要拿出所有的數據在內存中進行分頁。

所以我們在定義 key 的時候,就可以這么玩:

比如,一個人在pc端,store001店鋪下查看了spu001的商品,查看日期是 2020-06-09

key = user+pc+store001

value = spu001+2020-06-09

sorce = System.currentTimeMillis()

這里日期到天就好,使用 System.currentTimeMillis() 來進行精確到毫秒的排序。

SortedSet實現工具類

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Set;

/**
 * @author leizige
 */
@Component
public class RedisUtil{

    @Resource
    private ZSetOperations<String, String> zSetOperations;

    private final Long EMPTY = 0L;

    /**
     * 添加一個元素, zset與set最大的區別就是每個元素都有一個score,因此有個排序的輔助功能;  zadd
     * key,value已存在,score覆蓋
     *
     * @param key
     * @param value
     */
    public boolean add(String key, String value, double score) {
        return zSetOperations.add(key, value, score);
    }


    /**
     * 查詢集合中指定順序的值  zrevrange
     * <p>
     * 返回有序的集合中,score大的在前面 
     * 
     * @param key
     * @param offset
     * @param count
     * @return
     */
    public Set<String> reverseRangeByScore(String key, int offset, int count) {
        return zSetOperations.reverseRangeByScore(key, 1, Long.MAX_VALUE, (offset - 1) * count, count);
    }

    /**
     * ZCARD key
     * <p>
     * 返回有序集 key 的基數。
     * <p>
     * 可用版本:
     * >= 1.2.0
     * 時間復雜度:
     * O(1)
     * 返回值:
     * 當 key 存在且是有序集類型時,返回有序集的基數。
     * 當 key 不存在時,返回 0
     *
     * @param key
     * @return
     */
    public Long zCard(String key) {
        return zSetOperations.zCard(key);
    }

    /**
     * 刪除元素 zrem
     *
     * @param key
     * @param value
     */
    public Long remove(String key, String value) {
        return zSetOperations.remove(key, value);
    }


    /**
     * 移除有序集 key 中,指定排名(rank)區間內的所有成員。
     * <p>
     * 區間分別以下標參數 start 和 stop 指出,包含 start 和 stop 在內。
     * <p>
     * 下標參數 start 和 stop 都以 0 為底,也就是說,以 0 表示有序集第一個成員,以 1 表示有序集第二個成員,以此類推。
     * 你也可以使用負數下標,以 -1 表示最后一個成員, -2 表示倒數第二個成員,以此類推。
     * 可用版本:
     * >= 2.0.0
     * 時間復雜度:
     * O(log(N)+M), N 為有序集的基數,而 M 為被移除成員的數量。
     *
     * @param key
     * @param start
     * @param end
     * @return 被移除成員的數量
     */
    public Long removeRange(String key, long start, long end) {
        return zSetOperations.removeRange(key, start, end);
    }
}

實現原理

為了保證 Redis 中數據量的大小,限制每個用戶足跡最多保存 100 條記錄,最長保存 30 天。

新增足跡

使用System.currentTimeMillis()作為SortedSet的score來排序,並且 Set 天然支持去除重復數據,使用 ItemCode+LocalDate.now() 作為key,可以避免當天重復瀏覽一個商品,但Redis中只保存一條記錄。

void add(){
    @Value("${browsingHistory.maxSize:100}")		
    private Long maxSize;

    /**
     * 默認過期時長,單位:秒
     */
    private final static int DEFAULT_EXPIRE = 60 * 60 * 24 * 30;

    public String set(String key,String value) {
        redisUtil.add(key, value,System.currentTimeMillis());
		
		//獲取數量
        Long size = redisUtil.zCard(key);

        //如果最大數量超過配置的,就把超出的那一個干掉
        if(size > maxSize){
            Long removeRange = redisUtil.removeRange(key, 0,0);
        }

        redisUtils.expire(key, DEFAULT_EXPIRE);
        return key;
    }
}

查詢足跡

先查看該用戶有沒有瀏覽歷史,數量為0直接返回

在從 Redis 中取出數據的時候就進行分頁,避免在內存中進行分頁操作。

void query(){
    public Page<ItemResDto> queryAll(String key,int currentPage, int pageSize) {
        Long size = redisUtil.zCard(key);
        if (null == size || size.equals(0L)) {
            return new Pager<>();
        }

		//這里將緩存中的商品編碼拿出來,組裝商品信息后返回
        Set<String> values = redisUtil.reverseRangeByScore(key, currentPage, pageSize);

        return new Page<>(newItemResDtoList, size, pageSize, currentPage);
    }
}

清空足跡

public void removeAll(String key) {
    redisUtils.delete(key);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM