本文章主要讲述如何使用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. iOS开发基础146-深入解析WKWebView

    WKWebView是苹果在iOS 8中引入的重要组件,它替代了UIWebView,为开发者提供了高性能.高稳定性的网页显示和交互能力.在本文中,我们将深入探讨WKWebView的底层架构.关键特性.使 ...

  2. If Messi doesn't understand how to respect others, then he also doesn't deserve to receive respect from others.

    If Messi doesn't understand how to respect others, if he doesn't understand the spirit of honoring c ...

  3. Trump 黑马 or 搅局者? 讲座视频分享

    沈逸-特朗普能走多远   https://www.bilibili.com/video/BV1r7411t7VS/?spm_id_from=333.788.videocard.2     国际关系 对 ...

  4. 大连人工智能计算平台——华为昇腾AI平台——高性能计算HPC——如何在MPI中支持multiprocessing和fork操作——如何在HPC平台上使用pytorch——是否可以通过调度器的提交参数绕过HPC的计费系统

    本文要讨论的就是如何在MPI中支持multiprocessing和fork操作,但是这个问题同时也是如何在HPC平台如何使用pytorch的问题,可以说这两个问题其实是同一个问题,而这个问题的解决过程 ...

  5. Longley数据集——强共线性的宏观经济数据,包含GNP deflator(GNP平减指数)、GNP(国民生产总值)、Unemployed(失业率)、ArmedForces(武装力量)、Population(人口)、year(年份),Emlpoyed(就业率)。LongLey数据集因存在严重的多重共线性问题,在早期经常用来检验各种算法或计算机的计算精度

    Longley数据集来自J.W.Longley(1967)发表在JASA上的一篇论文,是强共线性的宏观经济数据,包含GNP deflator(GNP平减指数).GNP(国民生产总值).Unemploy ...

  6. 2. 基于MCU应用的EMC指南

    1. 硬件 主要的噪声感受器和发生器是印刷电路板(PCB)上的轨道和布线,特别是MCU附近的轨道和布线.因此,防止噪声问题的第一步行动涉及到PCB布局和电源设计.一般来说,MCU周围的元器件数量越少, ...

  7. Django集成腾讯COS对象存储

    前言 最近遇到一个场景需要把大量的资源文件存储到 OSS 里,这里选的是腾讯的 COS 对象存储 (话说我接下来想搞的 SnapMix 项目也是需要大量存储的,我打算搭个 MinIO 把 24T 的服 ...

  8. Ubuntu 安裝 RIME 輸入法

    RIME (Rime Input Method Engine,中州韻,中州韵)是一款很火的輸入法,虽然我目前还不知道它为什么火,不过先用用再说. 首先要吐槽一下 RIME 的说明文档,我感觉有点乱,第 ...

  9. Redis解读(3):Redis分布式锁、消息队列、操作位图进阶应用

    Redis 做分布式锁 分布式锁也算是 Redis 比较常见的使用场景 问题场景: 例如一个简单的用户操作,一个线城去修改用户的状态,首先从数据库中读出用户的状态,然后 在内存中进行修改,修改完成后, ...

  10. Python新手爬虫四:爬取视频

    老样子,先上最后成功源码(在D盘下创建'好看视频'文件夹,直接运行即可获取视频): import sys import re,os import requests from you_get impor ...