背景:
最近接到个需求,要求汇总用户一个月内在不同的供应商处购买的商品sku(上限1000条),去重后按照购买时间去倒序排序,当用户再次在对应的供应商处购买商品时,弹出之前的购买sku列表。
分析:
这个需求,简化后就是要求对用户(User)和供应商(Supplier) 做一个唯一键,在这个唯一键上,按照时间倒序去汇总好去重后的(Sku)。略微思考,使用redis的zset可以比Mysql,或者es更为方便处理。zset本身就能实现去重的功能,且对于同一元素进行重复赋值,会更新score;排序可以通过使用购买时间的timestamp作为score 来实现;保证一个唯一键只有一个月内1000条历史数据,可以通过zremrangebyscore和zremrangebyrank两个zset自带方法实现。
核心代码:
简化后的商品sku购买历史数据库DO
public class SccPurchaseDetailDO implements Serializable {
/**
* ID
*/
private Long id;
/**
* 订单号
*/
private String bizOrderNo;
/**
* 商品行ID, 同一个订单号, 可以采购多条sku, itemId用于区分该订单号下不同sku购买排序
*/
private Integer itemId;
/**
* 二级类目id
*/
private Long skuId;
/**
* 创建时间
*/
private Date gmtCreate;
}
简化后的zset缓存计算逻辑
/**
* 创建zset的key - 含供货商信息
*
* @param userId 用户id
* @param supplierEntId 供应商企业id
* @return
*/
private String makePayeeZSetKey(Long userId, Long supplierEntId) {
String key = userId + "t" + supplierEntId;
return RedisConstance.PURCHASE_DETAIL_ITEM_STATISTIC_ZSET_KEY + Md5Utils.getMD5(key.getBytes());
}
/**
* 将购买记录存入zset
*
* 对于同一个批次的购买记录, 支付时间是相同的
* @param detailList 在一次购买中,所有sku购买记录历史列表
* @param userId 购买人
* @param supplierEntId 供应商企业id
* @param paidTime 此次购买的支付时间
*/
private void handlePurchaseRecord(List<SccPurchaseDetailDO> detailList, Long userId, Long supplierEntId, Date paidTime) {
if (detailList.isEmpty()) {
return;
}
String payeeKey = makePayeeZSetKey(userId, supplierEntId);
long finishTime = paidTime.getTime();
Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
for (SccPurchaseDetailDO d : detailList) {
// itemId 从1-999, 代表此次购买中, 不同sku先后购买顺序
// 这里留了3个位给同一个购买批次中的不同sku
double score = finishTime * 1000 + d.getItemId() + 0.0D;
ZSetOperations.TypedTuple<String> r = new DefaultTypedTuple<>(d.getSkuId().toString(), score);
tuples.add(r);
}
redisTemplate.opsForZSet().add(payeeKey, tuples);
// 防止那些就买一次的人, 数据一直存在redis中
redisTemplate.expire(payeeKey, 30, TimeUnit.DAYS);
// 删除过期数据
redisTemplate.opsForZSet().removeRangeByScore(payeeKey, 0, oneMonthAgo * 1000 + 999);
Long size = redisTemplate.opsForZSet().zCard(payeeKey);
if (size != null && size > maxSize) {
// 排序是时间越小的 越前面
redisTemplate.opsForZSet().removeRange(payeeKey, 0, size - maxSize);
}
}
简化后的数据获取代码
// score是按照时间戳去赋值的
// 按照时间从近到远, 获取对应条数的sku采购历史
redisTemplate.opsForZSet().reverseRange(makePayeeZSetKey(request), start, end)
总结:
redis的zset,在进行一些去重排序的工作时,是非常有用的,特别是对一个相同的zset中的元素,重复赋值,会更新score,省去了我们对于元素是否已经存在的判断。