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

推荐以下好文:
详解单体架构 微服务 微服务架构 微服务各个组件 分布式 集群 负载均衡

微服务springcloud环境下基于Netty搭建websocket集群实现服务器消息推送----netty是yyds

2.5万字详解23种设计模式—创建型模式(简单工厂、工厂方法、抽象工厂、单例-多线程安全详解、建造者、原型)的详细解读、UML类图、及代码演示

一、引入问题

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

二、分析问题

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依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.1.8.RELEASE</version>
</dependency>

2.application.yml配置

server:
  port: 8889
  servlet:
    context-path: /

spring:
  redis:
    host: 192.168.0.41
    port: 6379
    password: wdy
    database: 2
    timeout: 5000

3.Product商品实体

@Data
public class Product implements Serializable {

	//商品id
    private Long id;

	//商品名称
    private String productName;

    //.....等属性
}

4.用户最近搜索信息

@Data
public class UserRecentSearch implements Serializable {

    /**
     * 搜索信息
     */
    private String searchInfo;

    /**
     * 用户id
     */
    private Long unionId;

5.redis辅助类SearchRedisHelper

@Component
public class SearchRedisHelper {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 热搜的key
     */
    public static final String HOT_SEARCH = "product_hot_search";

    /**
     * 最近搜索的key
     */
    public static final String RECENT_SEARCH = "product_recent_search";

    /**
     * 最近搜索的大小
     */
    public static final Integer CURRENT_SEARCH_SIZE = 3;

    /**
     * 热搜key的过期时间
     */
    public static final Integer HOT_SEARCH_EXPIRE_TIME = 3;

    /**
     * 设置redis的过期时间
     * expire其实是懒加载,不设置key的时候是不会执行的
     */
    @PostConstruct
    public void setHotSearchExpireTime() {
        redisTemplate.expire(HOT_SEARCH, HOT_SEARCH_EXPIRE_TIME, TimeUnit.SECONDS);
    }

    /**
     * redis添加最近搜索
     * @param query
     */
    public void addRedisRecentSearch(String query) {
        UserRecentSearch userRecentSearch = new UserRecentSearch();
        //用户id 当前用户 
        userRecentSearch.setUnionId(100434L);
        //搜索信息
        userRecentSearch.setSearchInfo(query);
        //score为一个分值,需要把最近浏览的商品id 的分值设置为最大值,
        //此处我们可以设置为当前时间Instant.now().getEpochSecond()
        //这样最近浏览的商品id的分值一定最大,排在ZSet集合最前面。
        ZSetOperations<String, UserRecentSearch> zSet = redisTemplate.opsForZSet();
        //由于zset 的集合特性当插入已经存在的 v 值 (商品id) 时只会更新score 值,
        zSet.add(RECENT_SEARCH, userRecentSearch, Instant.now().getEpochSecond());

        //获取到全部用户的最近搜索记录,用reverseRangeWithScores方法,可以获取到根据score排序之后的集合
        Set<ZSetOperations.TypedTuple<UserRecentSearch>> typedTuples = zSet.reverseRangeWithScores(RECENT_SEARCH, 0, -1);

        //只得到当前用户的最近搜索记录,注意这里必须保证set集合的顺序
        Set<UserRecentSearch> userRecentSearches = listRecentSearch();
        
        if (userRecentSearches.size() > CURRENT_SEARCH_SIZE) {
            //获取到最开始浏览的第一条
            UserRecentSearch userRecentSearchLast = userRecentSearches.stream().reduce((first, second) -> second).orElse(null);
            //删除最开始浏览的第一条
            zSet.remove(RECENT_SEARCH, userRecentSearchLast);
        }
    }

    /**
     * 热搜列表
     * @return
     */
    public Set<Product> listHotSearch() {
        //0 5 表示0-5下标对应的元素
        return redisTemplate.opsForZSet().reverseRangeWithScores(HOT_SEARCH, 0, 5);
    }

    /**
     * redis添加热搜
     * @param productList
     */
    public void addRedisHotSearch(List<Product> productList) {
        //1:表示每调用一次,当前product的分数+1
        productList.forEach(product -> redisTemplate.opsForZSet().incrementScore(HOT_SEARCH, product, 1D));
    }

    /**
     * 最近搜索列表
     * @return
     */
    public Set<UserRecentSearch> listRecentSearch() {
        Set<ZSetOperations.TypedTuple<UserRecentSearch>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(RECENT_SEARCH, 0, -1);
        return Optional.ofNullable(typedTuples)
                .map(tuples -> tuples.stream()
                        .map(ZSetOperations.TypedTuple::getValue)
                        .filter(Objects::nonNull)
                        .filter(userRecentSearch -> Objects.equals(userRecentSearch.getUnionId(), ContextHolder.getUser().getId()))
                        .collect(Collectors.collectingAndThen(
                                Collectors.toCollection(LinkedHashSet::new), LinkedHashSet::new)))
                .orElseGet(LinkedHashSet::new);
    }
}

6.业务service

@Service
public class ProductService {

    @Resource
    private SearchRedisHelper searchRedisHelper;

    /**
     * 搜索
     * @param query
     * @return
     */
    public List<Product> search(String query) {
        //业务代码可用es.....此处略过....模拟数据库数据
        List<Product> productList = new ArrayList();
        Product product = new Product();
        product.setId(1L);
        product.setProductName("iphone13");
        productList.add(product);
        searchRedisHelper.addRedisRecentSearch(query);
        searchRedisHelper.addRedisHotSearch(productList);
        return productList;
    }

    /**
     * 热搜列表
     * @return
     */
    public Set<Product> listHotSearch() {
        return searchRedisHelper.listHotSearch();
    }

    /**
     * 最近搜索列表
     * @return
     */
    public Set<UserRecentSearch> listRecentSearch() {
        return searchRedisHelper.listRecentSearch();
    }
}

7.controller控制层

@RequestMapping("/redis/test")
@RestController
public class RedisController {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private ProductService productService;

    /**
     * 删除redis
     * @param key
     * @return
     */
    @GetMapping("/w/remove/redis")
    public Result removeRedis(String key){
        redisTemplate.delete(key);
        return Result.success();
    }

    /**
     * 搜索
     * @param query
     * @return
     */
    @GetMapping("/r/search/product")
    public Result listProduct(String query) {
        return Result.success(productService.search(query));
    }

    /**
     * 热搜列表
     * @return
     */
    @ResponseBody
    @GetMapping("/r/list/hot/search")
    public Result listHotSearch() {
        return Result.success(productService.listHotSearch());
    }

    /**
     * 最近搜索列表
     * @return
     */
    @ResponseBody
    @GetMapping("/r/list/recent/search")
    public Result recentHotSearch() {
        return Result.success(productService.listRecentSearch());
    }

}

五、postman测试

1.第一次搜索

在这里插入图片描述

2.热点搜索

在这里插入图片描述

3.最近搜索

在这里插入图片描述

4.第二次第三次搜索

在这里插入图片描述
在这里插入图片描述

5.再看热点搜索

在这里插入图片描述

6.再看最近搜索变化

在这里插入图片描述

7.第四次搜索

在这里插入图片描述

8.热搜变化

在这里插入图片描述

9.最近搜索变化

在这里插入图片描述

六、总结

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

推荐以下好文:
详解单体架构 微服务 微服务架构 微服务各个组件 分布式 集群 负载均衡

微服务springcloud环境下基于Netty搭建websocket集群实现服务器消息推送----netty是yyds

2.5万字详解23种设计模式—创建型模式(简单工厂、工厂方法、抽象工厂、单例-多线程安全详解、建造者、原型)的详细解读、UML类图、及代码演示

如果看到这里,说明你喜欢这篇文章,请转发,点赞。关注微信公众号微信搜索[老板来一杯java]回复[进群]或者扫描下方二维码即可进入无广告交流群!回复[java]即可获取java基础经典面试一份!

  • 7
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Redis可以通过使用事务或Lua脚本来实现高并发下的抢购/秒杀功能。 1. 使用事务:抢购/秒杀的过程可以看做是一个先检查库存是否充足,再扣减库存的过程。使用Redis的事务可以保证这个过程是原子性的,即要么全部成功,要么全部失败。 具体实现方法如下: - 使用MULTI命令开启一个事务。 - 使用WATCH命令对库存进行监视(即设置监视器)。 - 使用GET命令获取当前库存。 - 判断库存是否充足,如果充足,则使用DECRBY命令扣减库存。 - 使用EXEC命令提交事务,如果提交成功,则说明扣减成功,否则说明被其他线程抢先扣减了库存。 示例代码如下: ```python def decrease_stock(redis_conn, stock_key): with redis_conn.pipeline() as pipeline: while True: try: pipeline.watch(stock_key) stock = int(pipeline.get(stock_key)) if stock > 0: pipeline.multi() pipeline.decr(stock_key) pipeline.execute() return True else: return False except WatchError: continue ``` 2. 使用Lua脚本:Lua脚本可以在Redis端原子性地执行多个命令,可以减少网络开销和锁竞争的问题。 具体实现方法如下: - 编写一个Lua脚本,该脚本首先使用GET命令获取当前库存,如果库存充足,则使用DECRBY命令扣减库存,否则返回0。 - 在Python中使用Redis的EVAL命令执行该Lua脚本。 示例代码如下: ```python def decrease_stock(redis_conn, stock_key): script = """ local stock = tonumber(redis.call('GET', KEYS[1])) if stock > 0 then redis.call('DECRBY', KEYS[1], 1) return 1 else return 0 end """ result = redis_conn.eval(script, 1, stock_key) return bool(result) ``` 以上两种方法都可以实现高并发下的抢购/秒杀功能,但是使用Lua脚本的方法效率更高,因为在Redis端执行命令可以减少网络开销和锁竞争的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王德印

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值