第六节 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());
}
}