Loading

使用 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);
}
If you’re going to reuse code, you need to understand that code!
posted @ 2020-09-14 17:50  不颓废青年  阅读(1116)  评论(0编辑  收藏  举报