本文章主要讲述如何使用Redis实现幂等、防抖、限流等功能。

幂等组件

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component; import java.util.Objects;
import java.util.concurrent.TimeUnit; /**
* 消息队列幂等处理器
*/
@Component
@RequiredArgsConstructor
public class MessageQueueIdempotentHandler { private final StringRedisTemplate stringRedisTemplate; private static final String IDEMPOTENT_KEY_PREFIX = "xxx:idempotent:"; /**
* 判断当前消息是否消费过
*
* @param messageId 消息唯一标识
* @return 消息是否消费过
*/
public boolean isMessageBeingConsumed(String messageId) {
String key = IDEMPOTENT_KEY_PREFIX + messageId;
return Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "0", 2, TimeUnit.MINUTES));
} /**
* 判断消息消费流程是否执行完成
*
* @param messageId 消息唯一标识
* @return 消息是否执行完成
*/
public boolean isAccomplish(String messageId) {
String key = IDEMPOTENT_KEY_PREFIX + messageId;
return Objects.equals(stringRedisTemplate.opsForValue().get(key), "1");
} /**
* 设置消息流程执行完成
*
* @param messageId 消息唯一标识
*/
public void setAccomplish(String messageId) {
String key = IDEMPOTENT_KEY_PREFIX + messageId;
stringRedisTemplate.opsForValue().set(key, "1", 2, TimeUnit.MINUTES);
} /**
* 如果消息处理遇到异常情况,删除幂等标识
*
* @param messageId 消息唯一标识
*/
public void delMessageProcessed(String messageId) {
String key = IDEMPOTENT_KEY_PREFIX + messageId;
stringRedisTemplate.delete(key);
}
}
@Component
@RocketMQMessageListener(consumerGroup = "saaslink_consumer_group", topic = RedisKeyConstant.SHORT_LINK_STATS_STREAM_TOPIC_KEY)
@Slf4j
public class ShortLinkStatsSaveConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt msgExt) {
String msgId = msgExt.getMsgId();
// 使用redis实现幂等
if (messageQueueIdempotentHandler.isMessageBeingConsumed(msgId.toString())) {
// 判断当前的这个消息流程是否执行完成
if (messageQueueIdempotentHandler.isAccomplish(msgId.toString())) {
return;
}
throw new ServiceException("消息未完成流程,需要消息队列重试");
}
try {
byte[] msgExtBody = msgExt.getBody();
// 转为map
Map<String, String> producerMap = JSON.parseObject(msgExtBody, Map.class);
ShortLinkStatsRecordDTO statsRecord = JSON.parseObject(producerMap.get("statsRecord"), ShortLinkStatsRecordDTO.class);
// 实际新增的逻辑
} catch (Throwable ex) {
// 某某某情况宕机了
messageQueueIdempotentHandler.delMessageProcessed(msgId.toString());
log.error("记录短链接监控消费异常", ex);
throw ex;
}
messageQueueIdempotentHandler.setAccomplish(msgId.toString());
}
}

防抖组件

幂等注解,防止用户重复提交表单信息,主要是通过分布式锁实现。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* 幂等注解,防止用户重复提交表单信息
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoDuplicateSubmit { /**
* 触发幂等失败逻辑时,返回的错误提示信息
*/
String message() default "您操作太快,请稍后再试";
}
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import com.nageoffer.onecoupon.framework.exception.ClientException;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Method; /**
* 防止用户重复提交表单信息切面控制器
*/
@Aspect
@RequiredArgsConstructor
public final class NoDuplicateSubmitAspect { private final RedissonClient redissonClient; /**
* 增强方法标记 {@link NoDuplicateSubmit} 注解逻辑
*/
@Around("@annotation(com.nageoffer.onecoupon.framework.idempotent.NoDuplicateSubmit)")
public Object noDuplicateSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
NoDuplicateSubmit noDuplicateSubmit = getNoDuplicateSubmitAnnotation(joinPoint);
// 获取分布式锁标识
String lockKey = String.format("no-duplicate-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,获取锁失败就意味着已经重复提交,直接抛出异常
if (!lock.tryLock()) {
throw new ClientException(noDuplicateSubmit.message());
}
Object result;
try {
// 执行标记了防重复提交注解的方法原逻辑
result = joinPoint.proceed();
} finally {
lock.unlock();
}
return result;
} /**
* @return 返回自定义防重复提交注解
*/
public static NoDuplicateSubmit getNoDuplicateSubmitAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
return targetMethod.getAnnotation(NoDuplicateSubmit.class);
} /**
* @return 获取当前线程上下文 ServletPath
*/
private String getServletPath() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return sra.getRequest().getServletPath();
} /**
* @return 当前操作用户 ID
*/
private String getCurrentUserId() {
// 从UserConText中获取
return "xxx";
} /**
* @return joinPoint md5
*/
private String calcArgsMD5(ProceedingJoinPoint joinPoint) {
return DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));
}

限流组件

Sentinel进行限流

/**
* 初始化限流配置
*/
@Component
public class SentinelRuleConfig implements InitializingBean { @Override
public void afterPropertiesSet() throws Exception {
List<FlowRule> rules = new ArrayList<>();
FlowRule createOrderRule = new FlowRule();
createOrderRule.setResource("xxx");
createOrderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
createOrderRule.setCount(1);
rules.add(createOrderRule);
FlowRuleManager.loadRules(rules);
}
}
/**
* 自定义流控策略
*/
public class CustomBlockHandler { public static Result<ShortLinkCreateRespDTO> createShortLinkBlockHandlerMethod(ShortLinkCreateReqDTO requestParam, BlockException exception) {
return new Result<ShortLinkCreateRespDTO>().setCode("B100000").setMessage("当前访问网站人数过多,请稍后再试...");
}
}
    @PostMapping("/api/xxx/v1/create")
@SentinelResource(
value = "xxx",
blockHandler = "createShortLinkBlockHandlerMethod",
blockHandlerClass = CustomBlockHandler.class
)
public Result<ShortLinkCreateRespDTO> create(@RequestBody CreateReqDTO requestParam) {
return Results.success(Service.create(requestParam));
}

Redis限流组件

通过lua脚本,判断1s以内的并发请求数是否超过我们的预期,如果超过我们的预计就进行限制。

-- 设置用户访问频率限制的参数
local username = KEYS[1]
local timeWindow = tonumber(ARGV[1]) -- 时间窗口,单位:秒 -- 构造 Redis 中存储用户访问次数的键名
local accessKey = "short-link:user-flow-risk-control:" .. username -- 原子递增访问次数,并获取递增后的值
local currentAccessCount = redis.call("INCR", accessKey) -- 设置键的过期时间
if currentAccessCount == 1 then
redis.call("EXPIRE", accessKey, timeWindow)
end -- 返回当前访问次数
return currentAccessCount
/**
* 用户操作流量风控配置文件
*/
@Data
@Component
@ConfigurationProperties(prefix = "xxx.flow-limit")
public class UserFlowRiskControlConfiguration { /**
* 是否开启用户流量风控验证
*/
private Boolean enable; /**
* 流量风控时间窗口,单位:秒
*/
private String timeWindow; /**
* 流量风控时间窗口内可访问次数
*/
private Long maxAccessCount;
}
xxx:
group:
max-num: 20
flow-limit:
enable: true
time-window: 1
max-access-count: 20
import com.alibaba.fastjson2.JSON;
import com.cmk.saaslink.admin.config.common.UserFlowRiskControlConfiguration;
import com.cmk.saaslink.common.convention.biz.user.UserContext;
import com.cmk.saaslink.common.convention.exception.ClientException;
import com.cmk.saaslink.common.convention.result.Results;
import com.google.common.collect.Lists;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource; import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional; import static com.cmk.saaslink.common.convention.errorcode.BaseErrorCode.FLOW_LIMIT_ERROR; /**
* 用户操作流量风控过滤器
*/
@Slf4j
@RequiredArgsConstructor
public class UserFlowRiskControlFilter implements Filter { private final StringRedisTemplate stringRedisTemplate;
private final UserFlowRiskControlConfiguration userFlowRiskControlConfiguration; private static final String USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH = "lua/user_flow_risk_control.lua"; @SneakyThrows
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH)));
redisScript.setResultType(Long.class);
String username = Optional.ofNullable(UserContext.getUsername()).orElse("other");
Long result;
try {
result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(username), userFlowRiskControlConfiguration.getTimeWindow());
} catch (Throwable ex) {
log.error("执行用户请求流量限制LUA脚本出错", ex);
returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
return;
}
if (result == null || result > userFlowRiskControlConfiguration.getMaxAccessCount()) {
returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
return;
}
filterChain.doFilter(request, response);
} private void returnJson(HttpServletResponse response, String json) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try (PrintWriter writer = response.getWriter()) {
writer.print(json);
}
}
}

Redis实现幂等、防抖、限流等功能的更多相关文章

  1. DBPack 限流熔断功能发布说明

    上周我们发布了 v0.4.0 版本,增加了限流熔断功能,现对这两个功能做如下说明. 限流 DBPack 限流熔断功能通过 filter 实现.要设置限流规则,首先要定义 RateLimitFilter ...

  2. Redis两种方式实现限流

    案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚本实现 private boolean accessLimit(String ip, int ...

  3. 【spring cloud】对接口调用者提供API使用的安全验证微服务【这里仅通过代码展示一种设计思想】【后续可以加入redis限流的功能,某段时间某个IP可以访问API几次】

    场景: 公司的微服务集群,有些API 会对外提供接口,供其他厂商进行调用.这些公开的API接口,由一个OpenAPI微服务统一提供给大家. 那么所有的调用者在调用公开API接口的时候,需要验证是否有权 ...

  4. 如何利用redis来进行分布式集群系统的限流设计

    在很多高并发请求的情况下,我们经常需要对系统进行限流,而且需要对应用集群进行全局的限流,那么我们如何类实现呢. 我们可以利用redis的缓存来进行实现,并且结合mysql数据库一起,先来看一个流程图. ...

  5. Redis实现的分布式锁和分布式限流

    随着现在分布式越来越普遍,分布式锁也十分常用,我的上一篇文章解释了使用zookeeper实现分布式锁(传送门),本次咱们说一下如何用Redis实现分布式锁和分布限流. Redis有个事务锁,就是如下的 ...

  6. 【Distributed】限流技巧

    一.概述 1.1 高并发服务限流特技 1.2 为什么要互联网项目要限流 1.3 高并发限流解决方案 二.限流算法 2.1 计数器 2.2 滑动窗口计数 2.3 令牌桶算法 使用RateLimiter实 ...

  7. spring中实现基于注解实现动态的接口限流防刷

    本文将介绍在spring项目中自定义注解,借助redis实现接口的限流 自定义注解类 import java.lang.annotation.ElementType; import java.lang ...

  8. 基于kubernetes的分布式限流

    做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ...

  9. 从-99打造Sentinel高可用集群限流中间件

    接上篇Sentinel集群限流探索,上次简单提到了集群限流的原理,然后用官方给的 demo 简单修改了一下,可以正常运行生效. 这一次需要更进一步,基于 Sentinel 实现内嵌式集群限流的高可用方 ...

  10. 分布式接口幂等性、分布式限流:Guava 、nginx和lua限流

    接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用. 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此 ...

随机推荐

  1. 【C】Re09 结构体

    一.结构体 Struct 创建和基本使用 #include <stdio.h> #include <stdlib.h> #include <string.h> // ...

  2. gym库中类FilterObservation(ObservationWrapper)的理解

    filter_observation.py模块中类 FilterObservation(ObservationWrapper) 的理解. 代码: import copy from gym import ...

  3. Apache DolphinScheduler:深入了解大数据调度工具

    一.海豚调度介绍 Apache DolphinScheduler 是一个分布式易扩展的可视化DAG工作流任务调度开源系统.适用于企业级场景,提供了一个可视化操作任务.工作流和全生命周期数据处理过程的解 ...

  4. 手把手教你掌握SeaTunnel k8s运行Zeta引擎本地模式的技巧

    转载自小虾米0.0 导读:随着Kubernetes的普及和发展,越来越多的企业和团队开始使用Kubernetes来管理和部署应用程序.然而,Kubernetes的默认工作方式可能不是最佳的选择,尤其是 ...

  5. RabbitMq消息可靠性之确认模式 通俗易懂 超详细 【内含案例】

    RabbitMq保证消息可靠性之确认模式 介绍 消息的确认,是指生产者投递消息后,如果 Broker 收到消息,则会给我们生产者一个应答.生产者进行接收应答,用来确定这条消息是否正常的发送到 Brok ...

  6. CANopen学习笔记(三)NMT

    NMT NMT主要用来管理和控制各个节点的状态,具体协议可以分为以下四类: NMT protocol Node guard protocol Heartbeat protocol Bootup pro ...

  7. Python自动复制Excel数据:将各行分别重复指定次数

      本文介绍基于Python语言,读取Excel表格文件数据,并将其中符合我们特定要求的那一行加以复制指定的次数,而不符合要求的那一行则不复制:并将所得结果保存为新的Excel表格文件的方法.   这 ...

  8. Java并发编程之验证volatile的可见性

    Java并发编程之验证volatile的可见性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥将通过代码 ...

  9. 【YashanDB知识库】开源调度框架Quartz写入Boolean值到YashanDB报错

    问题现象 Quartz 是一个广泛应用于企业级应用中的开源作业调度框架,它主要用于在Java环境中管理和执行任务. 为了任务调度,Quartz的数据模型中使用了大量的布尔值记录任务.流程的各种状态,如 ...

  10. 【YashanDB知识库】update/delete未选中行时,v$transaction视图没有事务,alter超时问题

    问题现象 1.alter table修改表字段名,卡住,超时. 2.查看v$transaction事务视图,没有看到事务记录. 3.问题单:调整表结构时超时 问题风险及影响 无风险 问题影响版本 客户 ...