• 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);
    }
    
    • 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

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

    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();//释放锁
       }
    }
    
    • 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

    对应的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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    但是通过阻塞队列来实现消息队列有缺陷:
    ①内存限制问题:因为创建阻塞队列是,需要初始大小,一旦在高并发的环境下,阻塞队列一下子就满了,那么这时候如果有订单到来,那么不会将这个订单存放到阻塞队列中
    ②数据安全的问题,因为是基于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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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();
                    }
                }
            }
        }
    
    
    • 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
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    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
    
    • 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

    发布以及查看探店笔记

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

    @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
        return blogService.saveBlog(blog);
    }
    
    • 1
    • 2
    • 3
    • 4

    BlogService接口对应的代码:

    Result saveBlog(Blog blog);
    
    • 1

    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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    查看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);
    }
    
    • 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

    点赞以及点赞排行榜

    要实现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));
    }
    
    • 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

    如果需要实现点赞排行榜,那么需要不仅仅需要保证点赞的用户不是重复的,同时需要保证点赞的用户是根据点赞的时间排序,那么这时候就可以利用到了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);
    }
    
    • 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
  • 相关阅读:
    数据库连接性比较:Navicat 和基于 Java 的工具
    学习黑马程序员JavaScript总结
    Linux基础指令(一)
    常见的一些Linux命令
    西安共享股东分红系统开发详细介绍
    聚观早报 | 极越07正式上市;宝骏云海正式上市
    华为云GaussDB打造金融行业坚实数据底座,共创数字金融新未来
    Chrome命令大全
    堆练习(二)— 抽奖系统
    【深度学习】torch-张量Tensor
  • 原文地址:https://blog.csdn.net/weixin_46544385/article/details/128184771