本文章主要讲述如何使用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. FFmpeg在游戏视频录制中的应用:画质与文件大小的综合比较

    我们游戏内的视频录制目前只支持avi固定码率,在玩家见面会上有玩家反馈希望改善录制画质,我最近在研究了有关视频画质的一些内容并做了一些统计. 录制视频大小对比 首先在游戏引擎中增加了对录制mp4格式的 ...

  2. 【转载】 NVIDIA RTX2080ti不支持P2P Access,这是真的么?

    原文地址: http://www.gpus.cn/gpus_list_page_techno_support_content?id=30 ------------------------------- ...

  3. vim 插件汇总网站

    在网上找到了一个vim插件的汇总网站,上面有对vim插件进行汇总.简介.使用排名等,十分适合vim用户在上面寻找一些可用的插件. 网址: https://vimawesome.com/ 虽然我没有太用 ...

  4. [CEOI2018] Lottery 题解

    前言 题目链接:洛谷. 题意简述 给出序列 \(a_1 \ldots a_n\) 和常数 \(l \leq n\),定义: \[\operatorname{dis}(i, j) = \sum _ {k ...

  5. C#自定义快捷操作键的实现 - 开源研究系列文章

    这次想到应用程序的快捷方式使用的问题. Windows已经提供了API函数能够对窗体的热键进行注册,然后就能够在窗体中使用这些注册的热键进行操作了.于是笔者就对这个操作进行了整理,将注册热键操作写成了 ...

  6. AvaloniaChat—从源码构建指南

    AvaloniaChat介绍 一个使用大型语言模型进行翻译的简单应用. 我自己的主要使用场景 在看英文文献的过程中,比较喜欢对照着翻译看,因此希望一边是英文一边是中文,虽然某些软件已经自带了翻译功能, ...

  7. python模块xlsxwriter使用

    1.安装 pip install XlsxWriter 2.使用 # -*- coding: utf-8 -*- from io import BytesIO import qrcode # impo ...

  8. Drools与动态加载规则文件

    Drools与动态加载规则文件 Drools简介 对系统使用人员来说: 对开发人员来说: Drools架构图 快速开始 Drools简介 Drools是一款基于Java的开源规则引擎,将规则与业务代码 ...

  9. Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤

    Dify简介 Dify是一个开源的大语言模型(Large Language Model, LLM)应用开发平台.它融合了后端即服务(Backend as a Service, BaaS)和LLMOps ...

  10. C++源码中司空见惯的PIMPL是什么?

    前言: C++源码中司空见惯的PIMPL是什么?用原始指针.std::unique_ptr和std::shared_ptr指向Implementation,会有什么不同?优缺点是什么?读完这篇文章,相 ...