系统设计-常见的点赞功能如何实现?如何防止刷赞?

点击上方名片关注我,为你带来更多踩坑案例

9732de68272d880b2aac9d4cfd06430e.png

- 引言 -

    如果你是一个摸爬滚打几年的开发者,那么这个阶段,对系统设计的合理性绝对是衡量一个人水平的重要标准。

    一个好的设计不光能让你工作中避免很多麻烦,还能为你面试的时候增加很多谈资

    而且,不同设计之间理念都是有借鉴性参考性的,你见过的设计多了,思考的多了,再次面临一个问题的时候,就会有很多点子不由自主的冒出来。

    希望这个系列的文章,能够和大家互相借鉴参考,共同进步。

- 常见的点赞如何实现? -

    每个人都见过点赞功能,大家想实现一个点赞功能也简单,比如一个简单的文章点赞逻辑如下:

    首先需要建个表,记录下点赞人的id,被点赞文章的id,点赞状态三个关键因素即可,需要的话可以把被点赞文章的作者id也冗余进来

    然后点赞的时候前端可以直接做一个状态的更改,无需等待后端结果返回,毕竟这是一个对数据精确性要求没那么高的功能

    至于后端的处理逻辑,可以根据以上三个字段,直接存到redis,做一个快速的读写,之后再异步保存到数据库中做一个备份即可(如果后续需要在别的地方如列表页查询,甚至可以直接在这里写到索引中)

    总之的原则就是,数据的快速读写,允许一定时间的误差(比如我点完赞非常快速的刷新页面发现还是未点赞状态,再次刷新的时候就是已点赞状态这种现象,是可以容忍的,虽然它也几乎不可能出现)


    相关的数据都可以存到redis,比如对某文章的点赞数

5d8451ad8ea2d818aeb844e6eb976a73.png

    某个用户对某篇文章的点赞关系

f8f3fa86f8594ed386832db79b333863.png

还有一些逻辑可以做判断优化,比如可以通过redis中的数据判断用户是否重复点赞,如果是的话就没必要再访问一次数据库了

    大致逻辑就是上面写的了,需要源码或者有问题的同学可以私信留言或者加我个人微信

- 如何防止刷赞? -

   哈哈大家其实可以随便打开一个网站,不停的点赞取消赞试试看,比如B站,在我狂点了几十秒后

c1826d02d310652a61f67763ba5c2b55.png

    它已经无法取消点赞了

    F12显示请求被拦截

a4f0398ae84bbfd389494fc2669570b8.png

之后再次刷新页面也无法取消,大概率是我这个ip被暂时限制了。

    可能不同的产品对此的方案也不一样,如果可以在网关做限流那是最好的,但是有些具体的业务在网关配置还是不太方便的

    下面给大家一个简单使用的防止刷赞的限流方法

     首先定义一个注解@Limit

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {


    /**
     * 资源的名字
     *
     * @return String
     */
    String name() default "";


    /**
     * 资源的key
     *
     * @return String
     */
    String key() default "";


    /**
     * Key的prefix
     *
     * @return String
     */
    String prefix() default "";


    /**
     * 给定的时间段
     * 单位秒
     *
     * @return int
     */
    int period();


    /**
     * 最多的访问限制次数
     *
     * @return int
     */
    int count();


    /**
     * 类型
     *
     * @return LimitType
     */
    LimitType limitType() default LimitType.CUSTOMER;


    // 限流方式,默认根据方法名methodName限流
    enum LimitType {
        /**
         * 自定义key
         */
        CUSTOMER,
        /**
         * 根据请求者IP
         */
        IP
    }
}

    它可以支持自定义key、ip限流以及根据方法名三种方式进行接口访问频次限流

    然后是注解拦截内容的处理逻辑,这部分代码太长就不贴了,贴一下主要逻辑,大家需要源码可以关注公众号加我

    主要的逻辑方法如下:   

@Around("execution(public * *(..)) && @annotation(com.luhui.utils.annotation.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String[] paramNames = signature.getParameterNames();
        Stream<?> stream = ArrayUtils.isEmpty(pjp.getArgs()) ? Stream.empty() : Arrays.stream(pjp.getArgs());
        List<Object> paramValues = stream
                .filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)))
                .collect(Collectors.toList());
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        Limit.LimitType limitType = limitAnnotation.limitType();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();
        switch (limitType) {
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = getLimitKeyValue(limitAnnotation.key(), paramNames, paramValues);
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
        try {
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number count = redisTemplate.execute(redisScript, keys, String.valueOf(limitCount), String.valueOf(limitPeriod));
            logger.info("Access try count is {} for name={} and key = {}", count, limitAnnotation.name(), key);
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                return ApiResponse.fail("访问太频繁,请稍后再试");
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                throw new RuntimeException(e.getLocalizedMessage());
            }
            throw new RuntimeException("server exception");
        }
    }

    具体的频次控制是通过redis的lua表达式来实现的。

    使用起来也很方便,比如我要限制点赞接口5分钟内不能超过10次,直接在接口上加注解即可

@Limit(key = "uid;assetsId", period = 300, count = 10, name="like", prefix = "limit_")

    最后就可以实现啦!

5e94d9febef0a4ab08ea538bc0cc0862.png

END

大家如果觉得有帮助,欢迎关注我!你们的关注就是我的动力,有什么好的建议也可以留言

猜你喜欢

转载自blog.csdn.net/qq_31363843/article/details/128030508
今日推荐