day5_redis学习

秒杀优化

上面的过程中,我们进行秒杀操作的基本步骤为:
在这里插入图片描述
所以这时候整个过程就耗费较长的时间,因为我们要判断用户是否已经购买了商品,需要查询数据库中表,同时也需要查询数据库得知库存数量,从而进行判断库存是否,每次查询都需要执行这2步,所以我们需要对这个过程进行优化,将商品的库存数量保存到redis中,同时将购买过这个商品的用户保存到redis中的set集合中,如果用户并没有存在这个商品订单的set集合中,说明没有购买。通过将这些数据保存到redis中,从而减少了数据库的查询次数
同时在上面的判断中可以得知用户是否有资格进行秒杀操作,如果有,那么就生成秒杀订单,这时候我们需要开启异步线程来生成订单。
所以整个过程为:
在这里插入图片描述
这时候就可以通过消息队列来实现异步线程中的任务了,如果消息队列中没有存在数据,那么不会生成订单,处于阻塞的状态,否则,就从消息队列中取出数据,然后生成订单。这里可以将介绍2种方式来实现消息队列:

阻塞队列实现消息队列

通过阻塞队列来实现异步秒杀的时候,上面的步骤中并不需要将用户的id,订单的id,以及商品的id保存到redis中,而是直接在扣减了库存,并且将当前用户添加到set集合中之后,直接返回0,表示当前的用户有资格进行秒杀此时就可以生成秒杀订单,然后修改数据库中的商品库存数量
因为在我们执行了Lua脚本之后,根据它的返回值来判断当前的用户是否有资格进行秒杀,如果有(返回值为0),那么我们就将新建一个VoucherOrder对象,然后添加到阻塞队列中,此时异步线程就可以从消息队列中取出一个VoucherOrder对象,来生成秒杀订单,同时更新数据库中的商品库存数量
所以对应的代码为:
秒杀接口优化后的代码为:

@Override
public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    //1、执行lua脚本,从而判断库存数量以及用户是否重复购买
    //如果返回的是0,说明用户秒杀成功,否则如果是1,说明库存不足
    //返回是2,表示用户重复购买
    Long result = stringRedisTemplate.execute(redisScript,
            Collections.emptyList(),
            //注意需要将voucherId,userId转成String,
            //因为stringRedisTemplate的key,value都是字符串类型的,否则
            //就抛出long cannot be cast to String
            voucherId.toString(),
            userId.toString()
    );
    if(result != 0){
        return Result.fail(result == 1 ? "库存不足" : "每个用户限购一件商品");
    }
    //2、返回的是0,那么将另外开启线程进行生成订单,并将订单id返回
    Long orderId = redisWorker.nextId("order");
    //3、创建VoucherOrder对象,并将其添加到阻塞队列中
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(orderId);
    voucherOrder.setVoucherId(voucherId);
    voucherOrder.setUserId(userId);
    queue.add(voucherOrder);
    return Result.ok(orderId);
}

阻塞队列生成秒杀订单代码:

private static final BlockingQueue<VoucherOrder> queue = new ArrayBlockingQueue<>(1024 * 1024);
//线程池,用于生成秒杀订单
private final ExecutorService service = Executors.newCachedThreadPool();
@PostConstruct
public void init(){
    //当构造方法执行完毕之后,就会执行这一步,来初始化线程任务
    //这样就可以保证一加载这个类,就可以执行线程任务了
    service.execute(new SeckillRunnable());
}

private class SeckillRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            try {
                //从阻塞队列中出去订单对象
                VoucherOrder voucherOrder = queue.take();
                //执行方法,来生成秒杀订单
                voucherOrderHandler(voucherOrder);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } 
    }
}

/**
* 通过阻塞队列来生成秒杀订单,为了避免redis服务器发生宕机,从而避免
* 在seckill.lua脚本中对库存数量,以及用户是否已经购买过的判断失效,
* 因此在当前的方法中利用redisson来实现分布式锁
*
* 然后可以获取分布式锁,就去生成订单,这时候需要注意商品超卖的问题,因此
* 需要利用乐观锁来解决商品超卖的问题
* @param voucherOrder
*/
@Transactional
public void voucherOrderHandler(VoucherOrder voucherOrder) {
   RLock lock = redissonClient.getLock("lock:voucherOrdre:userId:" + voucherOrder.getUserId());
   boolean isLock = lock.tryLock();
   try {
       if (!isLock) {
           //获取锁失败,那么直接返回
           log.error("每个用户限购1件商品");
           return;
       }
       Integer stock = seckillVoucherService.getById(voucherOrder.getVoucherId()).getStock();
       if (stock <= 0) {
           log.error("库存不足");
           return;
       }
       //获取锁成功之后,就可以进行秒杀商品了
       boolean isUpdate = seckillVoucherService.update(new UpdateWrapper<SeckillVoucher>().setSql("stock = stock - 1")
               .eq("voucher_id", voucherOrder.getVoucherId())
               .gt("stock", 0));
       if (!isUpdate) {
           log.error("库存不足");
           return;
       }
       //生成秒杀订单
       voucherOrderService.save(voucherOrder);
   }finally {
       lock.unlock();//释放锁
   }
}

对应的seckill.lua脚本的内容为:

--- 1、获取商品的id以及保存到redis中的商品的key
local voucherId = ARGV[1]
--- lua脚本中通过..进行拼接字符串的
local voucherKey = "hm_dianping:seckill:voucher:stock:"..voucherId
--- 2、获取用户的id以及商品订单的key
local userId = ARGV[2]
local orderId = ARGV[3]
local orderKey = "hm_dianping:seckill:order:voucher:"..voucherId
--- 3、获取商品的库存数量,判断是否充足
---这里需要利用tonumber,将返回值变成number类型,否则就会抛出异常attempt to compare boolean with number
if(tonumber (redis.call('get', voucherKey)) <= 0) then
    --- 库存不足
    return 1
end
--- 4、判断用户是否已经购买过这个商品了
if(tonumber(redis.call('sismember', orderKey, userId)) == 1) then
    --- 用户已经购买过了这个商品
    return 2
end
--- 5、更新库存,同时将这个用户添加到商品订单中,表示这个用户购买了这个商品
redis.call('incrby',voucherKey, -1)
redis.call('sadd', orderKey, userId)
return 0

但是通过阻塞队列来实现消息队列有缺陷:
①内存限制问题:因为创建阻塞队列是,需要初始大小,一旦在高并发的环境下,阻塞队列一下子就满了,那么这时候如果有订单到来,那么不会将这个订单存放到阻塞队列中
②数据安全的问题,因为是基于JVM的,如果服务器如果发生了宕机或者需要重启的时候,那么阻塞队列中的数据就会丢失(或者可以从阻塞队列是一个成员变量,每次重新启动,数据都是从0开始)。所以就有了下面的通过Redis来实现消息队列。

Redis实现消息队列

Redis中可以通过List,PubSub(发布-订阅模式),Stream这3种方式来实现消息队列。

List实现消息队列

其中List可以通过命令BLPOP或者BRPOP来获取元素,并且如果List为空的时候,可以在等待指定的时间之后才会返回null.从而实现阻塞,这样就可以实现了阻塞队列,同时解决了阻塞队列中存在的几个问题。
但是通过List实现消息队列存在几个问题:
①数据丢失异常:当从List中取出消息之后,那么就是将这个消息从List中删除,此时如果没有正确处理这一条消息,或者消息在中途丢失了,那么这时候我们是没有办法再从List中获取这一条数据
②只支持但消费者: 也即每一条消息,只能由一个人来获取,其他人不可以在获取

所以基于List实现的消息队列并不是我们的最优解。

PubSub实现消息队列

PubSub(发布-订阅):消费者可以订阅一个或者多个Channel(频道),生产者向对应的channel发布信息之后,那么订阅这个channel的消费者就可以收到消息,对应的命令有:

  • SUBSCRIBE channel : 订阅某一个频道
  • PUBLISH channel msg: 向某一个频道发布信息
  • PSUBSCRIBE pattern : 订阅符合pattern格式的频道
    显然PubSub已经可以支持了多消费者(也即一条消息可以被多个消费者获取),但是依旧存在几个问题:
    ①不支持消息持久化: 如果发送的消息没有被任何的消费者订阅,那么这个消息就会被丢弃,不会保存到redis中
    ②无法避免消息丢失异常
    ③消息堆积上限,超出时数据丢失

考虑到这些缺点,PubSub依旧不是解决我们问题的最优解。

Stream实现消息队列

Stream是Redis 5.0的一种新的数据结构,可以支持消息的持久化,并且拥有消费者组,以及消息确认机制,是一种功能比较完善的消息队列。
而通过Stream实现消息队列,常见的命令有:

  • XADD key [NOMKSTREAM] [MAXLEN | MINLEN] *|ID field value [fiele value, field value…]:表示向一个名字为key的消息队列添加一条消息,并且消息队列的消息数量为MAXLEN或者MINLEN,而NOMKSTREAM这个字段表示队列如果不存在,那么是否创建队列,默认是自动创建的* | ID表示的是消息的唯一ID,*表示由Redis来自动生成的,对应的格式为"时间戳-递增数字",而field value则为消息的内容,因为一条消息的内容可能有很多,所以消息内容是由消息体组成,而消息体则是以键值对的形式存在
  • XREAD [COUNT count] [BLOCK milisecond] STREAM key ID: 表示读取key这个消息队列中count条消息,如果消息队列为空,那么需要等待的时间为milisecond,如果没有设,那么直接返回null,如果为0,表示永久阻塞,一有消息不会再阻塞。
    ID则表示从消息队列中ID这一条消息开始读起,如果为0,表示从第一条消息读起,如果为$,表示从最新的消息开始读起,但是如果从最新的消息开始读起,那么就可能出现漏读的风险,因为我们现在读取一条消息,那么之后一次性添加许多条消息的时候,如果ID依旧是$,那么我们读取到的仅仅是最后一次添加的内容,从而出现了漏读

所以通过XREAD来读取Stream中的消息时,存在的特点为:
消息可回溯,因为我们并不像List那样,在获取消息的同时将这个消息从列表中删除。Stream获取消息,仅仅时读取消息,而不是删除消息
②一个消息可以支持多个消费者
③可以阻塞读取(XREAD COUNT BLOCK STREAM key ID)
④存在漏读风险(如果ID为$,那么读取到的时最新的消息,此时在调加多条数据的时候,就可能出现漏读)

所以就有了消费者组的形式,他将多个消费者分配给同一个消息队列,因此具有以下特点:
①多个消费者分配个同一个消息队列,那么消费者就会竞争队列中的消息,从而加快了消息处理的过程
②读取消息不再是从最新一条消息读起,消费者组会维护一个标识,记录最后一个被处理的消息,那么我们就从这个标识之后开始读取消息,从而确保每一条消息被读取,避免了漏读的情况
③消费者获取消息之后,消息处于pending状态,并存入到pendingList中,当消费者处理完,并且通过命令XACK发送确认之后,表示这一条消息被确认之后,那么就将这条消息从pendingList中移除

创建消费者组通过XGROUP CREATE来创建,然后通过XREADGROUP来读取,对应的命令为:

  • XGROUP CREATE key group_name ID mkstream
    其中key表示的是消息队列的名字,mkstrea则表示如果这个消息队列不存在,那么就会自动创建这个消息队列。group_name则是消费者组的名字
    ID是起始标示

  • XREADGROUP GROUP group_name consumer_name COUNT count Block milisecond STREAM key ID
    表示从key这个消息队列中的group_name消费者组读取count条消息,并且名字是consumer_name的消费者.如果消息队列为空,那么就阻塞milisecond毫秒。
    ID表示读取消息的起始,如果是>,表示从下一个未处理的消息开始读起,如果是其他,则表示从pendingList中获取已经处理但是没有确认的消息

  • XACK key group_name ID: 表示向ID这个消息发送确认,表示这个消息已经被处理完毕了

所以通过XGROUP来实现消息队列的特点为:
消息可回溯(数据持久化)
②一个消息可以支持多个消费者,并且消息队列中存在多个消费者,消费者之间竞争消息,加快消息处理过程
③可以支持阻塞读取
解决了漏读的风险,消费者组会维护一个标识(标记最后一个已经处理的消息),那么就会从这个标识后的消息开始读起,从而避免漏读
解决了消息丢失异常,因为可以通过XACK发送确认

所以Stream来实现消息队列时,我们在Lua脚本中判断了当前用户有资格进行秒杀的时候,需要将当前的订单id,用户id,以及商品id保存到消息队列中,通过XADD来添加。之后我们在异步线程中获取消息,然后生成秒杀订单,对应的代码为:
Stream实现秒杀接口代码为:

public Result seckillVoucher(Long voucherId) {
   //1、获取当前用户的登录id
   Long userId = UserHolder.getUser().getId();
   Long orderId = redisWorker.nextId("order");
   //2、获取订单id
   Long result = stringRedisTemplate.execute(
           redisScript,
           Collections.emptyList(),
           voucherId.toString(),userId.toString(), orderId.toString()
   );
   //3、如果result不等于0,说明抛出了异常
   if(result != 0){
       return Result.fail(result == 1 ? "库存不足" : "每个用户限购一件");
   }
   return Result.ok(orderId);
}

Stream实现的异步线程代码为:

//线程池,用于生成秒杀订单
    private final ExecutorService service = Executors.newCachedThreadPool();
    @PostConstruct
    public void init(){
        //当构造方法执行完毕之后,就会执行这一步,来初始化线程任务
        //这样就可以保证一加载这个类,就可以执行线程任务了
        service.execute(new SeckillRunnable());
    }

    private class SeckillRunnable implements Runnable{
        @Override
        public void run() {
            while(true){
                String group_name = "g1";
                try{
                    //1、从stream中取出消息XREADGROUP GROUP group_name consumer_name count 1 block 200 streams key >
                    List<MapRecord<String, Object, Object>> msgs = stringRedisTemplate.opsForStream().read(
                            //指定组名以及消费者的名字
                            Consumer.from(group_name, "c1"),
                            //指定获取1条消息,并且如果没有消息的时候,等待2秒
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            //指定读取的是哪一个key的消息,并且是从哪一条消息开始读
                            StreamOffset.create(RedisConstants.STREAM_ORDER_KEY, ReadOffset.lastConsumed())
                    );
                    if(msgs == null || msgs.isEmpty()){
                        //如果不存在消息,那么重新获取消息
                        continue;
                    }
                    //2、存在消息,解析数据
                    //key是一个消息的标识,而值是一个哈希值,因为一条消息不只一个消息体
                    MapRecord<String, Object, Object> msg = msgs.get(0);
                    //消息体
                    Map<Object, Object> msg_entries = msg.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(msg_entries, new VoucherOrder(), false);
                    //3、生成订单,同时扣减数据库中的库存量
                    voucherOrderHandler(voucherOrder);
                    //4、发送确认 XACK key group_name 消息的id
                    stringRedisTemplate.opsForStream().acknowledge(RedisConstants.STREAM_ORDER_KEY, group_name, msg.getId());
                } catch (Exception e){
                    //5、如果获取消息失败,那么这时候需要从pendingList中获取消息
                    handleMsgFromPendingList();
                }
            }
        }
    }

    /**
     * 从pendingList中获取没有确认的消息,对应的步骤为:
     */
    public void handleMsgFromPendingList() {
        while(true){
            String group_name = "g1";
            try{
                //1、从pendingList中获取未确认的消息
                List<MapRecord<String, Object, Object>> msgs = stringRedisTemplate.opsForStream().read(
                        //指定消费者组的名字以及消费者的名字
                        Consumer.from(group_name, "c1"),
                        //指定读取消息数量以及等待的时间
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        //指定读取的key以及从哪一条消息开始读起,由于是读取pendingList,所以从0
                        StreamOffset.create(RedisConstants.STREAM_ORDER_KEY, ReadOffset.from("0"))
                );
                if(msgs == null || msgs.isEmpty()){
                    //1.1 消息为空,那么直接退出循环
                    break;
                }
                //2、获取第一条消息(只获取1条)
                MapRecord<String, Object, Object> msg = msgs.get(0);
                //获取消息体
                Map<Object, Object> msg_entries = msg.getValue();
                //3、解析消息体,生成秒杀订单
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(msg_entries, new VoucherOrder(), false);
                voucherOrderHandler(voucherOrder);
                //4、发送确认
                stringRedisTemplate.opsForStream().acknowledge(RedisConstants.STREAM_ORDER_KEY, group_name, msg.getId());
            } catch (Exception e){
                log.info("处理pendingList异常....");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

Lua脚本为:

--- 1、获取商品的id以及保存到redis中的商品的key
local voucherId = ARGV[1]
--- lua脚本中通过..进行拼接字符串的
local voucherKey = "hm_dianping:seckill:voucher:stock:"..voucherId
--- 2、获取用户的id以及商品订单的key
local userId = ARGV[2]
local orderId = ARGV[3]
local orderKey = "hm_dianping:seckill:order:voucher:"..voucherId
--- 3、获取商品的库存数量,判断是否充足
---这里需要利用tonumber,将返回值变成number类型,否则就会抛出异常attempt to compare boolean with number
if(tonumber (redis.call('get', voucherKey)) <= 0) then
    --- 库存不足
    return 1
end
--- 4、判断用户是否已经购买过这个商品了
if(tonumber(redis.call('sismember', orderKey, userId)) == 1) then
    --- 用户已经购买过了这个商品
    return 2
end
--- 5、更新库存,同时将这个用户添加到商品订单中,表示这个用户购买了这个商品
redis.call('incrby',voucherKey, -1)
redis.call('sadd', orderKey, userId)
--- 6、将userId,voucherId,以及orderId保存到消息队列中
--- 因为将读取到的数据利用BeanUtil.fillBeanWithMap方法封装到VoucherOrder中
--- 所以消息体的名字和VoucherOrder的属性相同
local msg_key = "hm_dianping:stream:orders"
redis.call('xadd', msg_key,'*','userId',userId,'voucherId', voucherId, 'id', orderId)
return 0

发布以及查看探店笔记

发布探店笔记,那么首要需要保证blog中存在title,content以及关联商户,如下所示:
在这里插入图片描述
所以当我们点击发布按钮之后,就需要将这个新的blog添加到数据库中,所以对应的代码为:
发布Blog的接口代码:

@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
    return blogService.saveBlog(blog);
}

BlogService接口对应的代码:

Result saveBlog(Blog blog);

BlogServiceImpl实现的对应方法:

@Override
public Result saveBlog(Blog blog) {
    // 1、获取登录用户
    UserDTO user = UserHolder.getUser();
    Long userId = user.getId();
    blog.setUserId(userId);
    // 2、保存探店博文
    Boolean isSuccess = save(blog);
    if(BooleanUtil.isFalse(isSuccess)){
        return Result.fail("发布blog失败");
    }
    // 4、返回id
    return Result.ok(blog.getId());
}

查看blog,可以根据点赞比较高的blog,也可以点击查看某一篇blog,如下所示:
在这里插入图片描述
可以看到,无论是哪一种方式查看blog,都需要知道blog的作者,并且显示作者的头像,其次还会显示点赞按钮,此时如果当前的blog被当前的用户点赞,那么点赞按钮需要高亮,所以在获取blog数据之后,还需要查询blog的作者以及判断当前的blog是否被当前用户点赞,从而是否需要将点赞按钮变成高亮。
所以对应的代码为:

//获取所有的blog,并且根据点赞数降序排序
@Override
public Result queryHotBlog(Integer current) {
    // 根据点赞数降序排序,然后获取第current页的blog
    Page<Blog> page = query()
            .orderByDesc("liked")
            .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
    // 获取第current页数据
    List<Blog> records = page.getRecords();
    records.forEach(blog ->{
        //对于每一篇blog,需要获取它的作者信息以及判断是否被当前的用户点赞
        this.queryBlogUser(blog);
        this.isLikeByCurrentUser(blog);
    });
    return Result.ok(records);
}

//根据id来获取blog
@Override
public Result queryById(Long id) {
    Blog blog = getById(id);
    if(blog == null){
        return Result.fail("博客不存在");
    }
    //获取当前博客的作者
    queryBlogUser(blog);
    isLikeByCurrentUser(blog);
    return Result.ok(blog);
}

//获取blog的作者
public void queryBlogUser(Blog blog){
    User user = userService.getById(blog.getUserId());
    blog.setName(user.getNickName());
    blog.setIcon(user.getIcon());
}

//判断blog是否被当前的用户点赞,如果用户没有登录,默认没有被点赞
/**
 * 判断当前的博客是否已经被当前的用户点赞了
 * @param blog
 */
public void isLikeByCurrentUser(Blog blog) {
    UserDTO userDTO = UserHolder.getUser();
    if(userDTO == null){
        //用户没有登录,那么默认这个博客没有被当前访客点赞
        return;
    }
    Long userId = userDTO.getId();
    //判断当前的用户是否已经点赞过这个博客
    Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + blog.getId(), userId.toString());
    blog.setIsLike(score != null);
}

点赞以及点赞排行榜

要实现blog的点赞,那么我们需要明确点赞的需求:

  • 同一个用户只能点赞一次,当再次点赞的时候,就是取消电赞
  • 如果当前用户已经点赞了,那么点赞按钮需要试下高亮

对于第一条,不可以重复点赞,如果在已经点赞的前提下,再次点赞,那么就是取消点赞,此时我们将利用到redis中的Set数据结构,从而保证了用户不会重复点赞。

同时,如果用户已经点赞,那么点赞按钮需要实现高亮,那么这时候我们可以给Blog定义一个属性isLiked,表示当前这篇blog是否已经被当前的用户点赞了,如果为true,说明已经被点赞,所以高亮,否则不需要。

所以点赞的步骤为:
在这里插入图片描述
所以对应的代码为:

//点赞或者取消点赞某一篇blog
@Override
public Result likeBlog(Long id) {
     String userId = UserHolder.getUser().getId().toString();
     String blog_key = RedisConstants.BLOG_LIKED_KEY + id;
     //注意将userId转成String类型,因为使用的是StringRedisTemplate
     Boolean isMember = stringRedisTemplate.opsForSet().isMember(blog_key, userId);
     if(Boolean.TRUE.equals(isMember)){
         //2.1 如果已经点赞过了,那么再次点击的时候,需要将点赞数减1,并将当前的用户从set中移除
         Boolean isSuccess = update(new UpdateWrapper<Blog>().setSql("liked = liked - 1").eq("id", id));
         if(BooleanUtil.isTrue(isSuccess)){
             //更新redis中的set,将当前用户从set中删除
             stringRedisTemplate.opsForSet().remove(blog_key, userId);
         }
     }else{
         //2.2 没有点赞过,那么将当前的用户添加到set中,并且更新数据库的点赞数
         boolean isSuccess = update(new UpdateWrapper<Blog>().setSql("liked = liked + 1").eq("id", id));
         if(BooleanUtil.isTrue(isSuccess)){
             //数据库操作成功之后,才可以更新redis
             stringRedisTemplate.opsForSet().add(blog_key, userId);
         }
     }
     return Result.ok();
 }

//判断blog是否被当前的用户点赞了,如果没有登录,那么默认没有点赞
public void isLikeByCurrentUser(Blog blog) {
     UserDTO userDTO = UserHolder.getUser();
     if(userDTO == null){
        //用户没有登录,那么默认这个博客没有被当前访客点赞
        return;
     }
     Long userId = userDTO.getId();
   //判断当前的用户是否已经点赞过这个博客
     Boolean isMember = stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + blog.getId(), userId.toString());
     blog.setIsLike(BooleanUtil.isTrue(isMember));
}

如果需要实现点赞排行榜,那么需要不仅仅需要保证点赞的用户不是重复的,同时需要保证点赞的用户是根据点赞的时间排序,那么这时候就可以利用到了Redis中的排序集合ZSet,此时score属性就是点赞的时间。如果在点赞按钮的旁边还需要显示点赞的前5名,那么需要获取前5名点赞的人,然后获取这些用户的相关信息,然后在返回,所以对应的步骤是:
在这里插入图片描述
对应的代码为:

@Override
public Result likesBlogTop5(Long id) {
    String blog_key = RedisConstants.BLOG_LIKED_KEY + id;
    List<Long> userIds = stringRedisTemplate.opsForZSet().range(blog_key, 0, 4).stream()
                                                           .map(Long::valueOf) //将zset中的string类型的值转成Long类型
                                                           .collect(Collectors.toList());
    if(userIds == null || userIds.isEmpty()){
        //1.1 没有用户点赞过这个博客
        return Result.ok(Collections.emptyList());
    }
    //根据userIds,来查询用户,但是这时候listByIds是根据in子句查询的
    //所以在mysql中根据in子句查询的时候,得到的users对象并不是根据
    //上面的userIds排序的,也即导致users不是根据时间戳先后顺序排序
    //所以需要自定义排序顺序,使得是根据userIds排序的
    String idStr = StrUtil.join( ",",userIds);
    List<User> users = userService.query().in("id", userIds)
            //自定义排序顺序,使得id是根据idStr进行排序的,而idStr就是userIds中元素顺序
            .last("ORDER BY Field(id," + idStr + ")")
            .list()
            .stream()
            .collect(Collectors.toList());
    //2、由于User对象涉及到一些隐私信息,所以需要转成UserDTO
    List<UserDTO> userDTOs = users.stream()
            .map(user -> {
        return BeanUtil.copyProperties(user, UserDTO.class);
    })
            .collect(Collectors.toList());
    return Result.ok(userDTOs);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值