Redis+Lua脚本实现点赞功能demo

本文介绍了如何使用Redisson和Lua脚本实现在Web应用中处理用户对文章和评论的点赞功能,包括点赞计数、取消点赞及日志记录,同时保证了数据的一致性和性能优化。
摘要由CSDN通过智能技术生成

likedemo
欢迎访问我的网站谷流仓AI - guliucang.com

需求场景

用户可以对每篇文章点赞,也可以对文章下的评论点赞,页面需要展示文章点赞的总数和评论点赞的总数,点赞后可以取消点赞,取消点赞后点赞统计数量相应地减少,要求点赞功能不能直接打到数据库层。

demo代码

可以先跳过代码直接先看最下面的测试···环节。
首先目录结构如下
目录结构
redisson配置类:

package com.leo.like.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class RedissonConfig {

    @Bean
    public RedissonClient RedissonClient(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379")
                .setPassword("redisnopass");
        //使用json序列化方式(这里不设置的话,后面使用lua脚本就会出错,提示格式不对)
        Codec codec = new JsonJacksonCodec();
        config.setCodec(codec);
        return Redisson.create(config);
    }
}

启动类:

package com.leo.like;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LikeDemo {
    public static void main(String[] args) {
        SpringApplication.run(LikeDemo.class, args);
    }
}

先看LikeController接口:

package com.leo.like.controller;

import com.leo.like.service.LikeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("like")
public class LikeController {
    @Autowired
    LikeService likeService;

    @GetMapping("hit")
    public String like(@RequestParam("productId") Long productId,
                       @RequestParam("commentId") Long commentId,
                       @RequestParam("userId") Long userId) {
        Integer integer = likeService.like(commentId, productId, userId);
        return "success: " + integer;
    }
}

然后是LikeService

package com.leo.like.service;

public interface LikeService {
    Integer like(Long commentId, Long productId, Long userId);
}

然后是具体的实现类LikeServiceImpl

package com.leo.like.service.impl;

import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
import com.leo.like.enums.RedisKeys;
import com.leo.like.service.LikeService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Slf4j
@Service
public class LikeServiceImpl implements LikeService {
    @Autowired
    private RedissonClient redissonClient;

    private static String userCommentLikeScript = "";


    static {
        //获取lua脚本
        URL url = Resources.getResource("lua/like.lua");
        try {
            userCommentLikeScript = Resources.toString(url, StandardCharsets.UTF_8);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Override
    public Integer like(Long commentId, Long productId, Long userId) {
        Integer integer = updateMemoryAgreeAndCancelAgree(commentId, productId, userId);
        log.info("updateMemoryAgreeAndCancelAgree res:{}", integer);
        return integer;
    }

    public Integer updateMemoryAgreeAndCancelAgree(Long commentId, Long productId, Long userId) {
        List<Object> keys = ImmutableList.of(
                String.format(RedisKeys.USER_COMMENT_LIKES.getKeyPrefix(), userId, productId),
                String.format(RedisKeys.COMMENT_LIKES.getKeyPrefix(), productId),
                RedisKeys.COMMENT_LIKE_LOGS.getKeyPrefix()
        );

        Long resp = redissonClient.getScript().eval(
                (String) keys.get(0),
                RScript.Mode.READ_WRITE,
                userCommentLikeScript,
                RScript.ReturnType.INTEGER,
                keys,
                commentId,
                productId
        );
        log.info("resp 的结果是:{}", resp);
        if (resp == -1) {
            log.info("数据不存在, 插入缓存:{}, commentId:{}", keys.get(1), commentId);
            //下面这个数据一般是从数据库读出来,这里为了方便手动直接写死一个值,就假设数据库中这个评论的点赞数是23
            redissonClient.getMap((String) keys.get(1)).put(commentId, 23);
            resp = redissonClient.getScript().eval(
                    (String) keys.get(0),
                    RScript.Mode.READ_WRITE,
                    userCommentLikeScript,
                    RScript.ReturnType.INTEGER,
                    keys,
                    commentId,
                    productId
            );

        }
        return resp.intValue();

    }
}

然后是lua脚本文件:like.lua

-- 这个脚本用户操作点赞、取消点赞,记录点赞操作,统计点赞数量, 输出值是
-- redis lua 脚本可以输出日志 redis.log(redis.LOG_WARNING,"日志内容"),
-- 在redis.conf里面可以配置 loglevel 默认的是notice,具体有哪些选择可以直接去看conf文件
-- 通过配置logfile可以指定日志文件保存地址,默认是"",我这里本地的redis设置的是/data/redis.log,也可以通过docker logs直接查看redis日志,
-- 因为我这里这个docker redis的容器的data文件夹是挂载在我本地的 /Users/apple/dockerData/redis/data 的,
-- 所以可以直接在本地查看redis.log

--USER_COMMENT_LIKES("user_comment_likes:{%d}:%d","【用户侧评论赞成数量维护】(set结构,{%d}=userId,%d=productId,value=commentId)"),
local user_comment_likes_key = KEYS[1]
--COMMENT_LIKES("product_comment_likes:{%d}","【评论赞成数量维护】(hash结构,%d=productId,key=评论id,value=评论赞成数量)"),
local comment_like_key = KEYS[2]
--COMMENT_LIKE_LOGS("comment_like_logs","评论赞成数量日志】(list结构,value结构=productId:commentId|memoryAgreeCount(评论赞成数量))");
local comment_like_logs_ley = KEYS[3]
--评论id
local commentId = ARGV[1]
--商品id
local productId = ARGV[2]
--flag: 0取消点赞 1点赞 -1不存在商品,从数据库查
local flag = 0

local memoryAgreeCount = 0

redis.log(redis.LOG_WARNING,
'用户侧评论点赞数量维护链:', user_comment_likes_key,
'评论点赞数量维护链', comment_like_key,
'评论点赞数量日志', comment_like_logs_ley)

redis.log(redis.LOG_WARNING,
'评论id', commentId)

-- 评论点赞数量维护链不存在,查询数据库设置
if redis.call("HEXISTS", comment_like_key, commentId) ~= 1 then
    flag = -1
    return flag
end

--判断该用户之前是否点赞过
if redis.call("SISMEMBER", user_comment_likes_key, commentId) == 1 then
    redis.log(redis.LOG_WARNING, "该用户已经点赞过,执行取消点赞按钮")
    -- 从set中移除
    redis.call("SREM",user_comment_likes_key,commentId)
    memoryAgreeCount = redis.call("HINCRBY", comment_like_key, commentId,-1)
    flag = 0
else
    redis.log(redis.LOG_WARNING,"该用户没有点击过点赞按钮或者取消点赞了,执行点赞操作,将内存中的评论点赞数量进行加1")
    --加入到已点赞的set集合
    redis.call("SADD",user_comment_likes_key, commentId)
    --不为空将内存中的评论点赞数量即逆行加减
    redis.call("HINCRBY",comment_like_key, commentId, 1)
    memoryAgreeCount = redis.call("HGET", comment_like_key,commentId)
    flag = 1
end
redis.log(redis.LOG_WARNING, "获取内存中的点赞数量", memoryAgreeCount)
-- 记录日志列表
redis.call("RPUSH",comment_like_logs_ley, productId .. ":" .. commentId .. "|" .. memoryAgreeCount)
return flag

测试

我们来实际跑一下看看结果:

第一次请求

id为1的用户对id为13的商品下面的id为104的评论进行点赞

curl -X GET http://127.0.0.1:8080/like/hit?productId=13&commentId=104&userId=1

接口返回结果:success:1,表示点赞成功1次
然后查看一下redis记录的值

  • 首先是查看该商品下的每条评论的点赞总数, 数据结构是hash
#查询id=13的商品下面所有评论的总量
127.0.0.1:6379> HGETALL product_comment_likes:{13}
1) "[\"java.lang.Long\",104]"
2) "24"
127.0.0.1:6379> 

由于product_comment_likes:{13}这个key的存储结构是hash,所以可以看到上面查出来的结果是键值对的形式,结果显示评论id为104的点赞数量是24.为什么是24?因为上面代码写了,为了方便,直接假设从数据库读出来的历史点赞数量是23,所以加上这次点赞是24个点赞。

  • 然后查看该用户对商品id为13的下面的评论的点赞记录,数据结构是set:
# key是user_comment_likes:{1}:13, 代表id为1的用户对id为13的商品下面的评论的点赞记录
127.0.0.1:6379> SMEMBERS user_comment_likes:{1}:13
1) "[\"java.lang.Long\",104]"
127.0.0.1:6379> 

上面看出,id=1的用户对id=13的商品下面的评论的点赞只有一条记录,就是id为104的那条评论。

  • 然后查看点赞记录, 数据结构为List:
# LRANGE key startindex endindex, 下面表示获取comment_like_logs列表里面的所有元素
127.0.0.1:6379> LRANGE comment_like_logs 0 -1
1) "[\"java.lang.Long\",13]:[\"java.lang.Long\",104]|24"
127.0.0.1:6379> 

可以看到目前只有一条记录,就是商品13下面的评论104的点赞总数是24。

第二次请求

接下来,同样是用户1,对商品13下面id为105的评论进行点赞:

#请求
curl -X GET http://127.0.0.1:8080/like/hit?prodcutId=13&commentId=105&userId=1
#响应结果
success:1

然后来看一下结果:

  • 商品评论点赞数量统计
127.0.0.1:6379> HGETALL product_comment_likes:{13}
1) "[\"java.lang.Long\",104]"
2) "24"
3) "[\"java.lang.Long\",105]"
4) "24"
127.0.0.1:6379> 

可以看到评论105的点赞数量也是24了(前面说过了所有评论的初始点赞数量都设定了是23)。

  • 用户点赞记录:
127.0.0.1:6379> SMEMBERS user_comment_likes:{1}:13
1) "[\"java.lang.Long\",105]"
2) "[\"java.lang.Long\",104]"
127.0.0.1:6379> 

可以看到该用户对105的评论进行点赞了

  • 点赞日志:
127.0.0.1:6379> LRANGE comment_like_logs 0 -1
1) "[\"java.lang.Long\",13]:[\"java.lang.Long\",104]|24"
2) "[\"java.lang.Long\",13]:[\"java.lang.Long\",105]|24"
127.0.0.1:6379> 

可以看到记录了评论105的点赞数量

第三次请求

同样是用户1,对商品13下面id为104的评论取消点赞。点赞还是取消点赞是在lua脚本里面进行判断的,不清楚的话可以再回上面看一下代码。

#请求
curl -X GET http://127.0.0.1:8080/like/hit?prodcutId=13&commentId=104&userId=1
#响应结果
success:0

我们看到已经去掉点赞了,然后在看看redis

商品评论点赞数量统计:

127.0.0.1:6379> HGETALL product_comment_likes:{13}
1) "[\"java.lang.Long\",104]"
2) "23"
3) "[\"java.lang.Long\",105]"
4) "24"
127.0.0.1:6379> 

可以看到104的点赞已经变成23了。

  • 用户点赞记录:
127.0.0.1:6379> SMEMBERS user_comment_likes:{1}:13
1) "[\"java.lang.Long\",105]"
127.0.0.1:6379> 

可以看到这个set只剩下105这条评论的点赞了

  • 点赞日志:
127.0.0.1:6379> LRANGE comment_like_logs 0 -1
1) "[\"java.lang.Long\",13]:[\"java.lang.Long\",104]|24"
2) "[\"java.lang.Long\",13]:[\"java.lang.Long\",105]|24"
3) "[\"java.lang.Long\",13]:[\"java.lang.Long\",104]|23"
127.0.0.1:6379> 

可以看到记录了评论104的点赞数量变回23了

这样就实现了点赞的日志记录,点赞的数量统计,以及取消点赞

总结

从上面我们可以看出,通过利用redis的lua的原子性操作,高效的实现了点赞取消点赞的功能,然后只需要写个定时任务,每天比如说半夜把数据写入数据库就行了,点赞的反复操作基本不会打到数据库

  • 23
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值