- 普通的点赞如何实现? -
每个人都见过点赞功能,大家想实现一个点赞功能也简单,比如一个简单的文章点赞逻辑如下:
首先需要建个表,记录下点赞人的id,被点赞文章的id,点赞状态三个关键因素即可,需要的话可以把被点赞文章的作者id也冗余进来
冗余一个作者id字段这样子可以避免一个连表查询操作,在较大数据量下的时候,冗余字段带来的内存消耗的性价比是远远高于连表查询时带来的时间消耗的性价比的
然后点赞的时候前端可以直接做一个状态的更改,无需等待后端结果返回,毕竟这是一个对数据精确性要求没那么高的功能
首先需要两张数据库表:
article文章表
article_user_thumb文章用户点赞关系表,用于记录哪个用户点赞了哪个文章:
后端逻辑的实现
查询数据库article表给已有点赞数进行+1操作
问题:
注意,这里可能有线程安全性问题,实际上这是一个并发问题,只要在并发的情况下就会出现问题,我们知道Spring Mvc是基于servlet的,servlet在接收到用户请求后会从线程池中拿一个线程分配给它,每个请求都是一个单独的线程。试想一下,如果A线程在执行完查询操作后,发现没有记录,随后由于CPU调度,把控制权让了出去,然后B线程执行查询,也发现没有记录,这时候A和B线程都会执行保存并商品点赞数加1这个操作,导致数据不正确。
问题解决:
(其实上述问题主要的出现的原因在于将查询和加1操作进行了分步,而没有做到原子操作)
那么我们可以通过以下代码 / SQL实现一个原子的累加操作:
productService.update(new UpdateWrapper<article>().lambda()
.setSql("like_count = like_count + 1")
.eq(Article::getAuthorId, authorId));
= UPDATE article SET like_count = like_count + 1 WHERE author_id = ?;
因为在MySQL中执行DML操作的时候会自动上一个行级锁(前提是条件字段是被索引修饰),这样子可以保证其的SQL原子性
新增article_user_thumb表的一条数据进行关联
缓存性能优化
为了避免频繁地访问数据库,可以使用缓存技术Redis,将点赞量存储在缓存中。每次用户点赞时,首先将点赞量从缓存中读取,然后对其进行修改(该两部分操作使用lua脚本实现,保证整体操作的原子性),最后再将修改后的点赞量写回缓存。设置定时任务异步的将redis中的数据持久化到mysql
至于后端的处理逻辑,可以根据(点赞人的id,被点赞文章的id,点赞状态)三个字段,直接存到redis,做一个快速的读写,之后再异步保存到数据库中做一个备份即可(如果后续需要在别的地方如列表页查询,甚至可以直接在这里写到索引中)
总之的原则就是,数据的快速读写,允许一定时间的误差(比如我点完赞非常快速的刷新页面发现还是未点赞状态,再次刷新的时候就是已点赞状态这种现象,是可以容忍的,虽然它也几乎不可能出现)
相关的数据都可以存到redis,比如对某文章的点赞数
某个用户对某篇文章的点赞关系
还有一些逻辑可以做判断优化,比如可以通过redis中的数据判断用户是否重复点赞,如果是的话就没必要再访问一次数据库了
防止刷赞
防刷策略:
为了防止刷赞,可以实施以下几种策略:
- 限制点赞频率:可以限制用户在一定时间内的点赞次数,例如,每分钟或每小时点赞次数的限制。
- IP限制:可以限制同一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_")
最后就可以实现啦!
大家如果觉得有帮助,欢迎关注我!你们的关注就是我的动力,有什么好的建议也可以留言