一、引入问题

大家在浏览各种网站,比如淘宝,京东,微博等网站,都会看到一些热门搜索和最近搜索的功能,大家有木有好奇,技术背后是如何实现的呢?今天我们一起来用redis解决这两个问题,并已在项目中实战!!!
热搜如下图:
使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图1
最近搜索如下图:
使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图2

二、分析问题

1.热门搜索:是指一定时间、一定范围内,公众较为关心的热点问题,被搜索的次数越多,热搜榜越靠前。
2.最近搜索:是显示当前用户最近一段时间内搜索的记录,按照时间进行排序,如果有重复搜索,覆盖到重复的数据,并且要排到最前面。
3.针对于热门搜索属于高并发的场景,还需要高性能显示给用户,用Mysql存储显然不合适,流量过大会把mysql撑爆,最近搜索和热门搜索也不需要持久化,最好的解决方案之一就是redis做缓存,单机redis可以承受10万QPS。

三、针对于以上两个问题,使用redis怎么解决呢?

我们复习一下redis的五大数据类型

1. 字符串String

特性:
(1)最基本的数据类型,二进制安全的字符串,最大512M。
(2)支持字符串操作:strlen或取value的长度,返回的是字节的数量。
(3)数据交互有个二进制安全的概念,给我数据的时候你自己编码,字节数组到达我这里整理,帮你存,客户端之间商量好。
(4)支持数值计算操作:incr,decr
应用场景:做简单的键值对缓存,比如Session,token,统计,限流,轻量级(kb级别)的FS内存级的文件系统—任何东西都可以变成字节数组(二进制),一些复杂的计数功能的缓存

2.列表List

特性:
按照添加顺序保持顺序的字符串列表,也就是存储一些列表型得数据结构,类似粉丝列表、文字的评论列表之类的数据。
应用场景:
可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。

3.字典Hash

特性:
(1)key-value对的一种集合,存储结构化的数据,比如一个对象。
(2)这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。
应用场景:
经常会用来做用户数据的管理,存储用户的信息。比如做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

4.集合Set

特性:
无序的字符串集合,不存在重复的元素.
应用场景
去重,还可以利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

5.有序集合ZSet

特性:
已排序的字符串集合。去重并排序,如获取排名前几名。
应用场景:
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

6.需要解决的五大问题

问题一:很显然根据咱们的以上分析,热门搜索和最近搜索的功能需要去重并且排序,热门搜索点击率最高的在前面,最近搜索最新的数据搜索在最前面,所以使用ZSet集合实现最合适。针对于最近搜索的功能使用List也可以实现,但是删除的效率要比ZSet慢,还需要自己去重,所以还是Zset最合适。
问题二:用户可能无限制浏览商品,最近搜索的功能需要确保zSet 不能无限制插入,需要控制zSet 的大小,也就是指保存最近N条浏览记录。
问题三:最近搜索的功能需要在插入第N+1 条后移除最开始浏览的第一条。
问题四:热门搜索key值需要过期时间的。
问题五:热门搜索针对的是所有用户,而最近搜索针对的是当前用户。
以上五大问题均在代码中详细解决,仔细看注释。

四、编码实现

1.pom依赖

  1. <!-- redis -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. <version>2.1.8.RELEASE</version>
  6. </dependency>

2.application.yml配置

  1. server:
  2. port: 8889
  3. servlet:
  4. context-path: /
  5. spring:
  6. redis:
  7. host: 192.168.0.41
  8. port: 6379
  9. password: wdy
  10. database: 2
  11. timeout: 5000

3.Product商品实体

  1. @Data
  2. public class Product implements Serializable {
  3. //商品id
  4. private Long id;
  5. //商品名称
  6. private String productName;
  7. //.....等属性
  8. }

4.用户最近搜索信息

  1. @Data
  2. public class UserRecentSearch implements Serializable {
  3. /**
  4. * 搜索信息
  5. */
  6. private String searchInfo;
  7. /**
  8. * 用户id
  9. */
  10. private Long unionId;
  11. }

5.redis辅助类SearchRedisHelper

  1. @Component
  2. public class SearchRedisHelper {
  3. @Resource
  4. private RedisTemplate redisTemplate;
  5. /**
  6. * 热搜的key
  7. */
  8. public static final String HOT_SEARCH = "product_hot_search";
  9. /**
  10. * 最近搜索的key
  11. */
  12. public static final String RECENT_SEARCH = "product_recent_search";
  13. /**
  14. * 最近搜索的大小
  15. */
  16. public static final Integer CURRENT_SEARCH_SIZE = 3;
  17. /**
  18. * 热搜key的过期时间
  19. */
  20. public static final Integer HOT_SEARCH_EXPIRE_TIME = 3;
  21. /**
  22. * 设置redis的过期时间
  23. * expire其实是懒加载,不设置key的时候是不会执行的
  24. */
  25. @PostConstruct
  26. public void setHotSearchExpireTime() {
  27. redisTemplate.expire(HOT_SEARCH, HOT_SEARCH_EXPIRE_TIME, TimeUnit.SECONDS);
  28. }
  29. /**
  30. * redis添加最近搜索
  31. * @param query
  32. */
  33. public void addRedisRecentSearch(String query) {
  34. UserRecentSearch userRecentSearch = new UserRecentSearch();
  35. //用户id 当前用户
  36. userRecentSearch.setUnionId(100434L);
  37. //搜索信息
  38. userRecentSearch.setSearchInfo(query);
  39. //score为一个分值,需要把最近浏览的商品id 的分值设置为最大值,
  40. //此处我们可以设置为当前时间Instant.now().getEpochSecond()
  41. //这样最近浏览的商品id的分值一定最大,排在ZSet集合最前面。
  42. ZSetOperations<String, UserRecentSearch> zSet = redisTemplate.opsForZSet();
  43. //由于zset 的集合特性当插入已经存在的 v (商品id) 时只会更新score 值,
  44. zSet.add(RECENT_SEARCH, userRecentSearch, Instant.now().getEpochSecond());
  45. //获取到全部用户的最近搜索记录,用reverseRangeWithScores方法,可以获取到根据score排序之后的集合
  46. Set<ZSetOperations.TypedTuple<UserRecentSearch>> typedTuples = zSet.reverseRangeWithScores(RECENT_SEARCH, 0, -1);
  47. //只得到当前用户的最近搜索记录,注意这里必须保证set集合的顺序
  48. Set<UserRecentSearch> userRecentSearches = listRecentSearch();
  49. if (userRecentSearches.size() > CURRENT_SEARCH_SIZE) {
  50. //获取到最开始浏览的第一条
  51. UserRecentSearch userRecentSearchLast = userRecentSearches.stream().reduce((first, second) -> second).orElse(null);
  52. //删除最开始浏览的第一条
  53. zSet.remove(RECENT_SEARCH, userRecentSearchLast);
  54. }
  55. }
  56. /**
  57. * 热搜列表
  58. * @return
  59. */
  60. public Set<Product> listHotSearch() {
  61. //0 5 表示0-5下标对应的元素
  62. return redisTemplate.opsForZSet().reverseRangeWithScores(HOT_SEARCH, 0, 5);
  63. }
  64. /**
  65. * redis添加热搜
  66. * @param productList
  67. */
  68. public void addRedisHotSearch(List<Product> productList) {
  69. //1:表示每调用一次,当前product的分数+1
  70. productList.forEach(product -> redisTemplate.opsForZSet().incrementScore(HOT_SEARCH, product, 1D));
  71. }
  72. /**
  73. * 最近搜索列表
  74. * @return
  75. */
  76. public Set<UserRecentSearch> listRecentSearch() {
  77. Set<ZSetOperations.TypedTuple<UserRecentSearch>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(RECENT_SEARCH, 0, -1);
  78. return Optional.ofNullable(typedTuples)
  79. .map(tuples -> tuples.stream()
  80. .map(ZSetOperations.TypedTuple::getValue)
  81. .filter(Objects::nonNull)
  82. .filter(userRecentSearch -> Objects.equals(userRecentSearch.getUnionId(), ContextHolder.getUser().getId()))
  83. .collect(Collectors.collectingAndThen(
  84. Collectors.toCollection(LinkedHashSet::new), LinkedHashSet::new)))
  85. .orElseGet(LinkedHashSet::new);
  86. }
  87. }

6.业务service

  1. @Service
  2. public class ProductService {
  3. @Resource
  4. private SearchRedisHelper searchRedisHelper;
  5. /**
  6. * 搜索
  7. * @param query
  8. * @return
  9. */
  10. public List<Product> search(String query) {
  11. //业务代码可用es.....此处略过....模拟数据库数据
  12. List<Product> productList = new ArrayList();
  13. Product product = new Product();
  14. product.setId(1L);
  15. product.setProductName("iphone13");
  16. productList.add(product);
  17. searchRedisHelper.addRedisRecentSearch(query);
  18. searchRedisHelper.addRedisHotSearch(productList);
  19. return productList;
  20. }
  21. /**
  22. * 热搜列表
  23. * @return
  24. */
  25. public Set<Product> listHotSearch() {
  26. return searchRedisHelper.listHotSearch();
  27. }
  28. /**
  29. * 最近搜索列表
  30. * @return
  31. */
  32. public Set<UserRecentSearch> listRecentSearch() {
  33. return searchRedisHelper.listRecentSearch();
  34. }
  35. }

7.controller控制层

  1. @RequestMapping("/redis/test")
  2. @RestController
  3. public class RedisController {
  4. @Resource
  5. private RedisTemplate redisTemplate;
  6. @Resource
  7. private ProductService productService;
  8. /**
  9. * 删除redis
  10. * @param key
  11. * @return
  12. */
  13. @GetMapping("/w/remove/redis")
  14. public Result removeRedis(String key){
  15. redisTemplate.delete(key);
  16. return Result.success();
  17. }
  18. /**
  19. * 搜索
  20. * @param query
  21. * @return
  22. */
  23. @GetMapping("/r/search/product")
  24. public Result listProduct(String query) {
  25. return Result.success(productService.search(query));
  26. }
  27. /**
  28. * 热搜列表
  29. * @return
  30. */
  31. @ResponseBody
  32. @GetMapping("/r/list/hot/search")
  33. public Result listHotSearch() {
  34. return Result.success(productService.listHotSearch());
  35. }
  36. /**
  37. * 最近搜索列表
  38. * @return
  39. */
  40. @ResponseBody
  41. @GetMapping("/r/list/recent/search")
  42. public Result recentHotSearch() {
  43. return Result.success(productService.listRecentSearch());
  44. }
  45. }

五、postman测试

1.第一次搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图3

2.热点搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图4

3.最近搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图5

4.第二次第三次搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图6
使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图7

5.再看热点搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图8

6.再看最近搜索变化

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图9

7.第四次搜索

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图10

8.热搜变化

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图11

9.最近搜索变化

使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能 - 图12

六、总结

本文针对于网站热点搜索和最近搜索的问题,对redis的五大数据类型进行了解读,并且采用高并发利器redis的ZSet有序集合完美解决本文一开始引入的问题,保证了系统的高并发和高性能,提高用户体验。