第六节 SortedSet典型排行榜场景

一、心法

        Zset或者说是SortedSet,是Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。

        SortedSet不能重复,拥有一个权重score来从小到大排序。通过其可以排序的特点,往往应用于需要排序的场景。例如微博点击量排行榜,充值排行榜,学生成绩单排行。

二、如何在SortedSet中如何确定并叠加元素

        在同一个SortedSet中,如何才能判断出来操作的是同一个元素呢?找到它的目的是为了对其权重score进行操作。

        解决办法是在代码中,通常为某个对象创建一个DTO对象,这个DTO对象拥有一个唯一键作为标识。例如下方,使用手机号作为话费充值的唯一键。

@Data
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class BillDto implements Serializable {

    /**
     * 作为SortedSet的唯一键
     */
    private String phone;
}

@Data
@EqualsAndHashCode
public class Bill {
    private Integer id;
    @NotBlank
    private String phone;
    @NotNull
    private Double amount;
}

        已经为BillDto重写了HashCode与Equals方法。如果BillDto的电话号码phone一样,则在SortedSet中认为是同一个元素。因此就可以对同一个元素进行权重的操作。先在SortedSet中找到这个元素,然后使用SortedSet的操作权重的相关API,进行权重的增加。

 /**
     * 插入数据库
     * 失败,抛异常
     * 成功,加入缓存
     * 如果缓存中已经有此dto,则更新
     * 如果缓存中没有此dto,则添加
     */
    @Transactional(rollbackFor = Exception.class)
    public Integer addMoney(Bill bill) {
        bill.setId(null);
        int res = mapper.insertSelective(bill);
        if (res > 0) {
            //已重写HashCode与Equals方法,认为phone一样,在SortedSet中则是同一个对象
            BillDto dto = new BillDto(bill.getPhone());
            //获取指定元素的权重
            Double score = this.getZSetOperations().score(Constant.BILL_KEY, dto);
            if (score == null) {
                LOGGER.info("没有充值记录,加入缓存");
                //充值金额作为权重score,使用add加入SortedSet
                this.getZSetOperations().add(Constant.BILL_KEY, dto, bill.getAmount());
            } else {
                LOGGER.info("有充值记录,更新缓存");
                //incrementScore增加权重
                this.getZSetOperations().incrementScore(Constant.BILL_KEY, dto, bill.getAmount());
            }
        }
        return bill.getId();
    }

    private ZSetOperations<String, BillDto> getZSetOperations() {
        return redisTemplate.opsForZSet();
    }

        如何将SortedSet遍历出来呢?我这里的需求是将其从大到小遍历出来。因为SortedSet默认是根据权重从小到大进行排序,所以这里需要使用Reverse的相关API。

   public Map list() {
        //拿到SortedSet中所有的元素个数
        Long size = this.getZSetOperations().size(Constant.BILL_KEY);
        if (size == null || size == 0) {
            return new HashMap();
        }
        List<Bill> result = new ArrayList<>();
        //使用  Range进行遍历,附加条件是从大到小倒序,并且加上其元素权重
        Set<ZSetOperations.TypedTuple<BillDto>> setAsc =
                this.getZSetOperations().reverseRangeWithScores(Constant.BILL_KEY, 0, size);
        //构造结果集
        for (ZSetOperations.TypedTuple<BillDto> tuple : setAsc) {
            Bill bill = new Bill();
            //取出唯一键phone
            bill.setPhone(tuple.getValue().getPhone());
            //取出权重
            bill.setAmount(tuple.getScore());
            result.add(bill);
        }
        map.put("Amount Rank", result);
        return map;
    }

        大致效果如下。 

三、如何保证SortedSet与数据库中数据一致性

        缓存与数据库的数据需要一致。如果不一致,肯定是要以数据库数据作为标准。

        通常做一个定时任务来同步排行榜数据。先删除SortedSet缓存,然后从数据库中查询数据出来,塞入SortedSet缓存。

    @Async
    @Scheduled(cron = "*/30 * * * * ?")
    public void schedule() {
        LOGGER.info("从数据库同步话费充值数据");
        redisTemplate.delete(Constant.BILL_KEY);
        //根据电话号码分组
        List<Bill> list = billMapper.getAll();
        for (Bill bill : list) {
            BillDto dto = new BillDto(bill.getPhone());
            //dto只有一个phone属性,因此可以在SortedSet中唯一的标识此对象
            //另外就是,使用amount充值金额作为其权重score进行排序
            //已经通过SQL进行phone分组
            redisTemplate.opsForZSet().add(Constant.BILL_KEY, dto, bill.getAmount());
        }
    }

阅读更多 

        跟着大宇学Redis--------目录帖

posted @ 2022-07-17 12:14  小大宇  阅读(158)  评论(0编辑  收藏  举报