官网教程:https://www.redis.net.cn/tutorial/3505.html
菜鸟教程:https://www.runoob.com/redis/redis-tutorial.html
场景:
实现汽车热度排行榜
现有 Api 消费者服务、CarServer 提供者服务、Cache Redis缓存服务
1. 基本逻辑
数据预热:
第一次从 Api 进行访问请求 CarServer 的汽车热度榜单,CarServer 会优先请求 Cache 缓存看是否已被同步过数据。
如果没有,则为第一次访问,从 MySQL 数据库中读取并同步存储到 Cache 中。
如果有,则为非第一次访问,正常访问,可以直接请求 Cache 服务以达到更高效率的请求和返回结果。
总结就是:
- Cache redis服务中 SortedSet 只返回 set即可,map也行就是解析麻烦
- Cache 的 controller 的所有 get获取数据接口以
.toString()
返回,否则会被 ribbon 截断为1个元素(原因尚未可知)
- 其他服务接收到之后确保 接收到的 string 类型的 data 内有序不变,无需做其他操作
- 返回给 swagger 或 页面时只需将 string 类型的 data 按照 List 带
Feature.OrderedField
进行反解析即可 -> 保留原有有序集合的顺序返回
2. 代码实现
Api
JSONObject.parseObject(r.getData().toString(), List.class, Feature.OrderedField
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Api(tags = "汽车品牌查询接口") @RestController @RequestMapping("/api/brandlevecontroller/") public class BrandLeveController { @ApiOperation(value = "热度排行榜", notes = "根据车辆被关注的数量作为热度值进行降序排序") @GetMapping("leaderboard.do/{bid}") public R leaderboard(@PathVariable Integer bid) { return service.leaderboard(bid); } }
@Service public class BrandLeveServiceImpl implements BrandLeveService { @Autowired private RestTemplate restTemplate;
@Override public R leaderboard(Integer bid) { R r = restTemplate.getForObject("http://carserver/carserver/brandlevecontroller/leaderboard.do?bid=" + bid, R.class); List list = null; if (r != null) { list = JSONObject.parseObject(r.getData().toString(), List.class, Feature.OrderedField); } return R.ok(list); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @RestController @RequestMapping("/carserver/brandlevecontroller/") public class BrandLeveController { @Autowired private BrandLeveService service;
@GetMapping("leaderboard.do") public R leaderboard(@RequestParam Integer bid) { return service.leaderboard(bid); } }
@Service @Slf4j public class BrandLeveServiceImpl implements BrandLeveService { @Autowired private BrandLevelDao dao; @Autowired private RestTemplate restTemplate;
@Override public R leaderboard(Integer bid) { String key = CAR_LIST_KEY;
R checkKey = restTemplate.getForObject("http://cacheserver/cache/api/checkkey.do?key=" + key, R.class); if (checkKey != null && checkKey.getCode() == 200) { if ("false".equals(checkKey.getData())) { List<CarDto> carAttention = dao.getCarAttention(); if (carAttention != null) { for (CarDto dto : carAttention) { ZsetDto zset = new ZsetDto(); zset.setKey(key); zset.setTimes(60 * 60 * 24L); zset.setScore(dto.getAttention().doubleValue()); zset.setValue(JSON.toJSONString(dto));
HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<ZsetDto> requestEntity = new HttpEntity<>(zset, requestHeaders); restTemplate.postForObject("http://cacheserver/cache/api/savezset.do", requestEntity, R.class); }
R r = restTemplate.getForObject("http://cacheserver/cache/api/getzset.do?key=" + key + "&flag=" + 1, R.class); if (r != null) { return R.ok(r.getData()); } } } } return R.fail("排序失败"); } }
|
CacheServer
Ribbon 服务间传输需使用 String 来规避集合只能获取 1 个的问题;
获取有序集合时的标记设置,升降序通过 flag 传参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| @RestController @RequestMapping("cache/api") @Slf4j public class CacheController { @Autowired private CacheService service;
@PostMapping("/savezset.do") public R saveScoreSet2Redis( @RequestBody ZsetDto zsetDto) throws CacheException { return R.ok(service.saveScoreSet2Redis(zsetDto.getKey(), zsetDto.getTimes(),zsetDto.getScore() , zsetDto.getValue())); }
@GetMapping("/getzset.do") public R getScoreSetFromRedis(String key, int flag) { return R.ok(service.getScoreSetFromRedis(key, flag).toString()); } }
@Slf4j @Service @RefreshScope public class CacheServiceImpl implements CacheService { @Autowired private RedisTemplate<String, Object> template;
@Override public boolean saveScoreSet2Redis(String key, long expireTimeSeconds, double score, Object o) throws CacheException { try { template.opsForZSet().add(key, o, score); if (expireTimeSeconds > 0) { template.opsForSet().getOperations().expire(key, expireTimeSeconds, TimeUnit.SECONDS); } return true; } catch (Exception e) { log.error("存储异常"); throw new CacheException("存储异常" + e.getMessage()); } }
@Override public Set<Object> getScoreSetFromRedis(String key, int flag) { return flag == 0 ? template.opsForZSet().range(key, 0, -1) : template.opsForZSet().reverseRange(key, 0, -1); } @Override public boolean checkKey(String key) { return template.hasKey(key); } }
|
3. 成绩排行榜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
private void updateRedisRank(MpMockexam mpMockexam, MpUseranswerstat mpUseranswerstat, List<MpUserscorestat> mpUserscorestatList){ String redisKey = MockExamRedisKey.RKEY_MOCKEXAM_PAPERRANK + mpUseranswerstat.getMeId() + ":" + mpUseranswerstat.getPaperId(); BigDecimal reverseScore = BigDecimal.valueOf(mpUseranswerstat.getUasScoretotal()).subtract(BigDecimal.valueOf(mpUseranswerstat.getUasAnswerscore())); String redisScore = reverseScore.multiply(BigDecimal.valueOf(100)).intValue() + "." + String.format("%06d", mpUseranswerstat.getUasDuration()); Set<ZSetOperations.TypedTuple<Object>> rankSet = redisUtil.zrangebyscore(redisKey, 0L, Long.valueOf(mpMockexam.getMeRanknum())); for(ZSetOperations.TypedTuple typedTuple : rankSet) { MpUseranswerstatVo vo = JSON.parseObject(String.valueOf(typedTuple.getValue()), MpUseranswerstatVo.class); if (vo.getUserId().equals(mpUseranswerstat.getUserId())){ redisUtil.zrem(redisKey, typedTuple.getValue()); } } MpUseranswerstatVo vo = createRankObject(mpUseranswerstat, mpUserscorestatList); Set<ZSetOperations.TypedTuple<Object>> userSet = new HashSet<>(); DefaultTypedTuple<Object> userData = new DefaultTypedTuple<>(JSON.toJSONString(vo), Double.parseDouble(redisScore)); userSet.add(userData); redisUtil.zadd(redisKey, userSet); redisUtil.zremByRange(redisKey, Long.valueOf(mpMockexam.getMeRanknum()), redisUtil.zZCard(redisKey)); Long time = DateUtils.addDay(DateUtils.toDate(mpMockexam.getMeEnddate()), 7).getTime() - (new Date()).getTime(); if (time <= 0){ redisUtil.del(redisKey); } redisUtil.expire(redisKey, time); }
private MpUseranswerstatVo createRankObject(MpUseranswerstat mpUseranswerstat, List<MpUserscorestat> mpUserscorestatList){ MpUseranswerstatVo vo = new MpUseranswerstatVo(); vo.setPaperId(mpUseranswerstat.getPaperId()); vo.setUserId(mpUseranswerstat.getUserId()); vo.setPushType(mpUseranswerstat.getPushType()); vo.setUserName(mpUseranswerstat.getUserName()); vo.setUasScoretotal(mpUseranswerstat.getUasAnswerscore()); vo.setUasDuration(mpUseranswerstat.getUasDuration()); Map ussScore = Maps.newHashMap(); mpUserscorestatList.stream().forEach(mpUserscorestat -> ussScore.put(mpUserscorestat.getUssQuestiontype(), mpUserscorestat.getUssAnswerscore())); vo.setUssScore(ussScore); return vo; }
|
4. 限制排行榜最大长度
以下是使用 Jedis 客户端向 Redis 中添加元素并设置排行榜长度的示例代码: - redis 工具类同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Jedis jedis = new Jedis("localhost");
jedis.zadd("rank", 100, "user1"); jedis.zadd("rank", 200, "user2"); jedis.zadd("rank", 300, "user3");
long rankLength = jedis.zcard("rank");
if (rankLength > MAX_RANK_LENGTH) { jedis.zremrangeByRank("rank", 0, rankLength - MAX_RANK_LENGTH - 1); }
|
在上述代码中,我们首先通过 zadd
方法向有序集合中添加元素,并指定元素的分数。然后,通过 zcard
方法获取有序集合的长度,判断是否超出设定的最大值。如果有序集合长度超过了设定的最大值,则调用 zremrangeByRank
方法删除末尾的元素。
5. 获取指定排名的元素
使用 RedisTemplate 实现取指定排名位置的数据的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12
| java复制代码@Autowired private RedisTemplate<String, String> redisTemplate;
public Set<String> getRankingList(int start, int end) { redisTemplate.opsForZSet().add("rank", "user1", 100); redisTemplate.opsForZSet().add("rank", "user2", 200); redisTemplate.opsForZSet().add("rank", "user3", 300);
return redisTemplate.opsForZSet().range("rank", start, end); }
|
倒数排名则需要更换方法:
1 2
| return redisTemplate.opsForZSet().reverseRange("rank", start, end);
|
6. 获取指定元素的排名
如果想要获取指定成员的排名(即分数从高到低的排名),可以使用 reverseRank()
方法或 rank()
方法。例如:
1 2 3 4 5 6 7 8 9 10 11 12
| java复制代码@Autowired private RedisTemplate<String, String> redisTemplate;
public Long getUserRank(String user) { redisTemplate.opsForZSet().add("rank", "user1", 100); redisTemplate.opsForZSet().add("rank", "user2", 200); redisTemplate.opsForZSet().add("rank", "user3", 300);
return redisTemplate.opsForZSet().reverseRank("rank", user); }
|
在上述代码中,我们可以使用 reverseRank
方法获取指定成员的排名。如果要获取分数从低到高的排名,可以使用 rank
方法。