使用redis完成点赞和关注数据

需求描述

点赞和关注功能一般都会有排序的潜在需求 对应到redis就是有序集合zset 用时间戳表示分数
点赞为最早点赞的显示在前面 比如微信朋友圈点赞 所以直接取出即可
关注为最新的关注人显示在前面 所以要用倒序的命令reverseRange去取即可

点赞功能

sortedset存储方式

key: 前缀+blogId
value: 点赞的用户id
score: 当前时间戳

表结构设计

点赞表
CREATE TABLE `tb_user_blog_like` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `blog_id` bigint NOT NULL COMMENT '帖子id',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `created_at` timestamp NOT NULL COMMENT '创建时间',
  `updated_at` timestamp NOT NULL COMMENT '更新时间',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1点赞0未点赞',
  PRIMARY KEY (`id`),
  KEY `idx_user_id_blog_id` (`user_id`,`blog_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
关注表
CREATE TABLE `tb_follow` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint unsigned NOT NULL COMMENT '用户id',
  `follow_user_id` bigint unsigned NOT NULL COMMENT '被关联的用户id',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1关注0未关注',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_user_id_follow_user_id` (`user_id`,`follow_user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT

点赞接口:

@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
    blogService.like(id);
    return Result.ok();
}

service:

    /**
     * like posts
     *
     * @param blogId
     */
    @Override
    public void like(Long blogId) {
        Long userId = UserHolder.getUser().getId();
        DefaultRedisScript<Object> redisScript = new DefaultRedisScript<>("" +
                "local blogId = ARGV[1];" +
                "local userId = ARGV[2];" +
                "local blogKey = KEYS[1] .. blogId;" +
                "local messageQueueKey = KEYS[2];" +
                "local score = redis.call('zscore', blogKey, userId);" +
                "if(score == false) then " +
                "local time = redis.call('time');" +
                "local nowMills = time[1] * 1000 + math.floor(time[2] / 1000);" +
                "redis.call('zadd', blogKey, nowMills, userId);" +
                "else " +
                "redis.call('zrem', blogKey, userId); " +
                "end; " +
                "redis.call('xadd', messageQueueKey, '*', 'userId', userId, 'blogId', blogId);" +
                "");
        stringRedisTemplate.execute(redisScript, Arrays.asList(BLOG_LIKED, BLOG_USER_STREAMS_LIKE), blogId.toString(), userId.toString());
    }
    public boolean isLike(String userId, String blogId) {
        return Optional.ofNullable(stringRedisTemplate.opsForZSet().score(BLOG_LIKED + blogId, userId)).orElse(Double.valueOf(0)) > 0;
    }

    public int getLiked(String blogId) {
        return stringRedisTemplate.opsForZSet().count(BLOG_LIKED + blogId, 0, System.currentTimeMillis()).intValue();
    }

使用lua脚本保证一致性 如果有userId就删掉 没有就添加 然后发送消息到队列供消费端消费回写db
开启一个线程去轮询消费:

@PostConstruct
private void initJob() {
    threadPoolExecutor.submit(new BlogLikeJob());
}

private class BlogLikeJob implements Runnable {

    private boolean updateLike(Long blogId, Long userId) {
        RLock rLock = redissonClient.getLock("lock:blog:update:like:" + blogId);
        try {
            boolean tryLock = rLock.tryLock(2L, TimeUnit.SECONDS);
            if (tryLock) {
                UserBlogLike userBlogLike = new UserBlogLike();
                userBlogLike.setUserId(userId);
                userBlogLike.setBlogId(blogId);
                Integer status = userBlogLikeService.exist(userBlogLike);
                int delta = Objects.nonNull(status) && status == 1 ? -1 : 1;
                Boolean success = transactionTemplate.execute(transactionStatus -> {
                    long updated = blogMapper.updateLike(blogId, delta);
                    long toggleLike;
                    if (Objects.isNull(status)) {
                        toggleLike = userBlogLikeService.insert(userBlogLike);
                    } else {
                        userBlogLike.setStatus(status == 1 ? 0 : 1);
                        toggleLike = userBlogLikeService.update(userBlogLike);
                    }
                    if (updated < 1 || toggleLike < 1) {
                        if (!transactionStatus.isCompleted()) {
                            transactionStatus.setRollbackOnly();
                        }
                    }
                    return updated > 1 && toggleLike > 1;
                });
                return Boolean.TRUE.equals(success);
            }
            return false;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (rLock.isHeldByCurrentThread() && rLock.isLocked()) {
                rLock.unlock();
            }
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create(BLOG_USER_STREAMS_LIKE, ReadOffset.lastConsumed())
                );
                if (Objects.isNull(records) || records.isEmpty()) {
                    continue;
                }
                MapRecord<String, Object, Object> record = records.get(0);
                Map<Object, Object> entries = record.getValue();
                Long blogId = Long.valueOf((String) entries.get("blogId"));
                Long userId = Long.valueOf(entries.get("userId").toString());
                log.info("消费点赞数据: blogId: {}, userId: {}", blogId, userId);
                boolean success = this.updateLike(blogId, userId);
                if (success) {
                    stringRedisTemplate.opsForStream().acknowledge(BLOG_USER_STREAMS_LIKE, "g1", record.getId());
                }
            } catch (Exception e) {
                log.info("handle update blog like exception: {}", e);
                handlePendingList();
            }
        }
    }


    private void handlePendingList() {
        int retries = 5;
        while (retries-- > 0) { // loop read
            try {
                // there is equal to "execute the XREADGROUP GROUP g1 c1 COUNT 1 STREAMS seckill:voucher:streams.orders 0" commands
                List<MapRecord<String, Object, Object>> mapRecords = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"), // specify the group and customer
                        StreamReadOptions.empty().count(1), // specify the number of messages
                        StreamOffset.create(BLOG_USER_STREAMS_LIKE, ReadOffset.from("0")) // specify the streams which the messages read from, here are read from historical records
                );
                if (Objects.isNull(mapRecords) || mapRecords.isEmpty()) {
                    // messages pending queue is empty, end the process
                    break;
                }
                MapRecord<String, Object, Object> record = mapRecords.get(0); // get a id:entries map
                Map<Object, Object> entries = record.getValue();// get the entries map
                Long blogId = Long.valueOf(entries.get("blogId").toString());
                Long userId = Long.valueOf(entries.get("userId").toString());
                log.info("消费点赞数据: blogId: {}, userId: {}", blogId, userId);
                boolean success = this.updateLike(blogId, userId);
                if (success) {
                    stringRedisTemplate.opsForStream().acknowledge(BLOG_USER_STREAMS_LIKE, "g1", record.getId()); // acknowledge the success
                }
            } catch (Exception e) {
                log.info("handle pending-list update blog like exception: {}", e);
                try {
                    Thread.sleep(20); // re-read in 20ms later
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
        log.info("handle pending-list update blog like failure");
    }
}

public boolean isLike(String userId, String blogId) {
    return Optional.ofNullable(stringRedisTemplate.opsForZSet().score(BLOG_LIKED + blogId, userId)).orElse(Double.valueOf(0)) > 0;
}

public int getLiked(String blogId) {
    return stringRedisTemplate.opsForZSet().count(BLOG_LIKED + blogId, 0, System.currentTimeMillis()).intValue();
}

和redis涉及写的操作都用lua保证原子性
获取点赞top5:

/**
  * 查询点赞用户Top5
  *
  * @param id
  * @return
  */
 @Override
 public List<UserDTO> queryLikedUsers(Long id) {
     String key = BLOG_LIKED + id;
     Set<String> ids = stringRedisTemplate.opsForZSet().range(key, 0, 4);
     if (Objects.isNull(ids) || ids.isEmpty()) {
         return Collections.emptyList();
     }
     return userService.findByIds(ids.stream().collect(Collectors.toList()));
 }

关注操作和点赞操作一个道理 直接上代码:
关注接口:

@PutMapping("/{id}/{follow}")
public Result updateFollow(@PathVariable Long id, @PathVariable Integer follow) {
    followService.updateFollow(id, follow);
    return Result.ok();
}
@Override
public void updateFollow(Long followUserId, Integer status) {
    DefaultRedisScript<Object> redisScript = new DefaultRedisScript<>("" +
            "local userId = ARGV[1];" +
            "local userFollowKey = KEYS[1] .. userId;" +
            "local followMessageQueueKey = KEYS[2];" +
            "local followUserId = ARGV[2];" +
            "local status = ARGV[3];" +
            "local score = redis.call('zscore', userFollowKey, followUserId);" +
            "if(status == '1' and score == false) then " +
            "local time = redis.call('time');" +
            "local nowMills = time[1] * 1000 + math.floor(time[2] / 1000);" +
            "redis.call('zadd', userFollowKey, nowMills, followUserId);" +
            "end; " +
            "if(status == '0' and score ~= false) then " +
            "redis.call('zrem', userFollowKey, followUserId);" +
            "end;" +
            "redis.call('xadd', followMessageQueueKey, '*', 'userId', userId, 'followUserId', followUserId, 'status', status); " +
            "");
    stringRedisTemplate.execute(redisScript, Arrays.asList(USER_FOLLOWS, USER_STREAM_FOLLOW), UserHolder.getUser().getId().toString(), followUserId.toString(), status.toString());
}

查询是否关注了某用户:

@Override
public int isFollow(Long userId) {
    Double exists = stringRedisTemplate.opsForZSet().score(USER_FOLLOWS + UserHolder.getUser().getId().toString(), userId.toString());
    return Optional.ofNullable(exists).orElse(Double.valueOf(0)) > 0 ? 1 : 0;
}

查询和某用户的共同关注:

@Override
public List<UserDTO> commonFollowFromCache(Long followedUserId) {
    String followUserKey = USER_FOLLOWS + followedUserId;
    Set<String> followIds = stringRedisTemplate.opsForZSet().reverseRange(followUserKey, 0, -1);
    if (Objects.isNull(followIds) || followIds.isEmpty()) {
        return Collections.emptyList();
    }

    String currentUserKey = USER_FOLLOWS + UserHolder.getUser().getId();
    Set<String> currentIds = stringRedisTemplate.opsForZSet().reverseRange(currentUserKey, 0, -1);

    List<String> interactionIds = interact(followIds.stream().collect(Collectors.toList()), currentIds.stream().collect(Collectors.toList()));

    return userService.findByIds(interactionIds);
}

public <T> List<T> interact(List<T> list, List<T> other) {
    List<T> largerList = list.size() == other.size() ? list : (list.size() > other.size() ? list : other);
    List<T> smallerList = list.size() == other.size() ? other : (list.size() < other.size() ? list : other);

    int i = 0;
    int index = 0;
    while (index < smallerList.size()) {
        T currentId = smallerList.get(index);
        if (largerList.contains(currentId)) {
            smallerList.set(i, currentId);
            i++;
        }
        index++;
    }
    return smallerList.subList(0, i);
}

关注数据也要落库的 开启一个线程轮询消费:

@PostConstruct
private void initJob() {
    threadPoolExecutor.submit(new UserFollowJob());
}

private class UserFollowJob implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create(USER_STREAM_FOLLOW, ReadOffset.lastConsumed())
                );
                if (Objects.isNull(records) || records.isEmpty()) {
                    continue;
                }
                MapRecord<String, Object, Object> record = records.get(0);
                Map<Object, Object> entries = record.getValue();
                Long userId = Long.valueOf(entries.get("userId").toString());
                Long followUserId = Long.valueOf((String) entries.get("followUserId"));
                Integer status = Integer.valueOf((String) entries.get("status"));
                log.info("消费关注数据: userId: {}, followUserId: {}, status: {}", userId, followUserId, status);
                boolean success = this.updateFollow(userId, followUserId, status);
                if (success) {
                    stringRedisTemplate.opsForStream().acknowledge(USER_STREAM_FOLLOW, "g1", record.getId());
                }
            } catch (Exception e) {
                log.info("handle update user follow exception: {}", e);
                handlePendingList();
            }
        }
    }

    private boolean updateFollow(Long userId, Long followUserId, Integer status) {
        RLock lock = redissonClient.getLock("lock:user:update:follow:" + userId);
        try {
            boolean tryLock = lock.tryLock(2L, TimeUnit.SECONDS);
            if (tryLock) {
                Follow newOrUpdatedFollow = new Follow();
                newOrUpdatedFollow.setUserId(userId);
                newOrUpdatedFollow.setFollowUserId(followUserId);
                long exists = followMapper.exist(newOrUpdatedFollow);
                newOrUpdatedFollow.setStatus(status);
                if (exists > 0) {
                    // 如果存在 更新status
                    long updated = followMapper.updateFollow(newOrUpdatedFollow);
                    return updated > 0;
                } else {
                    // 不存在 新插入
                    long inserted = followMapper.insertFollow(newOrUpdatedFollow);
                    return inserted > 0;
                }
            }
            return false;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                lock.unlock();
            }
        }
    }

    private void handlePendingList() {
        int retries = 5;
        while (retries-- > 0) {
            try {
                List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create(USER_STREAM_FOLLOW, ReadOffset.from("0"))
                );
                if (Objects.isNull(records) || records.isEmpty()) {
                    break;
                }
                MapRecord<String, Object, Object> record = records.get(0);
                Map<Object, Object> entries = record.getValue();
                Long userId = Long.valueOf(entries.get("userId").toString());
                Long followUserId = Long.valueOf((String) entries.get("followUserId"));
                Integer status = Integer.valueOf((String) entries.get("status"));
                log.info("消费关注数据: userId: {}, followUserId: {}, status: {}", userId, followUserId, status);
                boolean success = this.updateFollow(userId, followUserId, status);
                if (success) {
                    stringRedisTemplate.opsForStream().acknowledge(USER_STREAM_FOLLOW, "g1", record.getId());
                }
            } catch (Exception e) {
                log.info("handle update user follow exception: {}", e);
                try {
                    Thread.sleep(20);
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
        log.info("handle update user follow failure");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值