Spring Boot实战:基于策略模式+代理模式手写幂等性注解组件

news2025/4/21 19:50:52

一、为什么需要幂等性?

核心定义:在分布式系统中,一个操作无论执行一次还是多次,最终结果都保持一致。
典型场景

  • 用户重复点击提交按钮
  • 网络抖动导致的请求重试
  • 消息队列的重复消费
  • 支付系统的回调通知

不处理幂等的风险

  • 重复创建订单导致资金损失
  • 库存超卖引发资损风险
  • 用户数据重复插入破坏业务逻辑

二、实现步骤分解

1. 定义幂等注解

/**
 * 幂等注解
 *
 * @author dyh
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * 幂等的超时时间,默认为 1 秒
     *
     * 注意,如果执行时间超过它,请求还是会进来
     */
    int timeout() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 提示信息,正在执行中的提示
     */
    String message() default "重复请求,请稍后重试";

    /**
     * 使用的 Key 解析器
     *
     * @see DefaultIdempotentKeyResolver 全局级别
     * @see UserIdempotentKeyResolver 用户级别
     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
     */
    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";

    /**
     * 删除 Key,当发生异常时候
     *
     * 问题:为什么发生异常时,需要删除 Key 呢?
     * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。
     *
     * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢?
     * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解
     */
    boolean deleteKeyWhenException() default true;

}

2. 设计Key解析器接口

/**
 * 幂等 Key 解析器接口
 *
 * @author dyh
 */
public interface IdempotentKeyResolver {

    /**
     * 解析一个 Key
     *
     * @param idempotent 幂等注解
     * @param joinPoint  AOP 切面
     * @return Key
     */
    String resolver(JoinPoint joinPoint, Idempotent idempotent);

}

3. 实现三种核心策略

  • 默认策略:方法签名+参数MD5(防全局重复)
  • 用户策略:用户ID+方法特征(防用户重复)
  • 表达式策略:SpEL动态解析参数(灵活定制)

3.1 默认策略


/**
 * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
 *
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author dyh
 */
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {

    /**
     * 核心方法:生成幂等Key(基于方法特征+参数内容)
     * @param joinPoint   AOP切入点对象,包含方法调用信息
     * @param idempotent  方法上的幂等注解对象
     * @return 生成的唯一幂等Key(32位MD5哈希值)
     */
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 获取方法完整签名(格式:返回值类型 类名.方法名(参数类型列表))
        // 示例:String com.example.UserService.createUser(Long,String)
        String methodName = joinPoint.getSignature().toString();

        // 将方法参数数组拼接为字符串(用逗号分隔)
        // 示例:参数是 [123, "张三"] 将拼接为 "123,张三"
        String argsStr = StrUtil.join(",", joinPoint.getArgs());

        // 将方法签名和参数字符串合并后计算MD5
        // 目的:将可能很长的字符串压缩为固定长度,避免Redis Key过长
        return SecureUtil.md5(methodName + argsStr);
    }

}

3.2 用户策略


/**
 * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
 * <p>
 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 *
 * @author dyh
 */
public class UserIdempotentKeyResolver implements IdempotentKeyResolver {

    /**
     * 生成用户级别的幂等Key
     *
     * @param joinPoint  AOP切入点对象(包含方法调用信息)
     * @param idempotent 方法上的幂等注解
     * @return 基于用户维度的32位MD5哈希值
     * <p>
     * 生成逻辑分四步:
     * 1. 获取方法签名 -> 标识具体方法
     * 2. 拼接参数值 -> 标识操作数据
     * 3. 获取用户身份 -> 隔离用户操作
     * 4. MD5哈希计算 -> 压缩存储空间
     */
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 步骤1:获取方法唯一标识(格式:返回类型 类名.方法名(参数类型列表))
        // 示例:"void com.service.UserService.updatePassword(Long,String)"
        String methodName = joinPoint.getSignature().toString();

        // 步骤2:将方法参数转换为逗号分隔的字符串
        // 示例:参数是 [1001, "新密码"] 会拼接成 "1001,新密码"
        String argsStr = StrUtil.join(",", joinPoint.getArgs());

        // 步骤3:从请求上下文中获取当前登录用户ID
        // 注意:需确保在Web请求环境中使用,未登录时可能返回null
        Long userId = WebFrameworkUtils.getLoginUserId();

        // 步骤4:获取当前用户类型(例如:0-普通用户,1-管理员)
        // 作用:区分不同权限用户的操作
        Integer userType = WebFrameworkUtils.getLoginUserType();

        // 步骤5:将所有要素拼接后生成MD5哈希值
        // 输入示例:"void updatePassword()1001,新密码1231"
        // 输出示例:"d3d9446802a44259755d38e6d163e820"
        return SecureUtil.md5(methodName + argsStr + userId + userType);
    }
}

3.3 表达式策略

/**
 * 基于 Spring EL 表达式,
 *
 * @author dyh
 */
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
    // 参数名发现器:用于获取方法的参数名称(如:userId, orderId)
    // 为什么用LocalVariableTable:因为编译后默认不保留参数名,需要这个工具读取调试信息
    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    // 表达式解析器:专门解析Spring EL表达式
    // 为什么用Spel:Spring官方标准,支持复杂表达式语法
    private final ExpressionParser expressionParser = new SpelExpressionParser();

    /**
     * 核心方法:解析生成幂等Key
     *
     * @param joinPoint  AOP切入点(包含方法调用信息)
     * @param idempotent 方法上的幂等注解
     * @return 根据表达式生成的唯一Key
     */
    @Override
    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
        // 步骤1:获取当前执行的方法对象
        Method method = getMethod(joinPoint);

        // 步骤2:获取方法参数值数组(例如:[订单对象, 用户对象])
        Object[] args = joinPoint.getArgs();

        // 步骤3:获取方法参数名数组(例如:["order", "user"])
        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);

        // 步骤4:创建表达式上下文(相当于给表达式提供变量环境)
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();

        // 步骤5:将参数名和参数值绑定到上下文(让表达式能识别#order这样的变量)
        if (ArrayUtil.isNotEmpty(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                // 例如:将"order"参数名和实际的Order对象绑定
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }

        // 步骤6:解析注解中的表达式(例如:"#order.id")
        Expression expression = expressionParser.parseExpression(idempotent.keyArg());

        // 步骤7:执行表达式计算(例如:从order对象中取出id属性值)
        return expression.getValue(evaluationContext, String.class);
    }

    /**
     * 辅助方法:获取实际执行的方法对象
     * 为什么需要这个方法:处理Spring AOP代理接口的情况
     *
     * @param point AOP切入点
     * @return 实际被调用的方法对象
     */
    private static Method getMethod(JoinPoint point) {
        // 情况一:方法直接定义在类上(非接口方法)
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (!method.getDeclaringClass().isInterface()) {
            return method; // 直接返回当前方法
        }

        // 情况二:方法定义在接口上(需要获取实现类的方法)
        try {
            // 通过反射获取目标类(实际实现类)的方法
            // 例如:UserService接口的create方法 -> UserServiceImpl的create方法
            return point.getTarget().getClass().getDeclaredMethod(
                    point.getSignature().getName(), // 方法名
                    method.getParameterTypes());    // 参数类型
        } catch (NoSuchMethodException e) {
            // 找不到方法时抛出运行时异常(通常意味着代码结构有问题)
            throw new RuntimeException("方法不存在: " + method.getName(), e);
        }
    }
}

4. 编写AOP切面

/**
 * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
 * 幂等切面处理器
 *
 * 功能:拦截被 @Idempotent 注解标记的方法,通过Redis实现请求幂等性控制
 * 流程:
 * 1. 根据配置的Key解析策略生成唯一标识
 * 2. 尝试在Redis中设置该Key(SETNX操作)
 * 3. 若Key已存在 → 抛出重复请求异常
 * 4. 若Key不存在 → 执行业务逻辑
 * 5. 异常时根据配置决定是否删除Key
 *
 * @author dyh
 */
@Aspect  // 声明为AOP切面类
@Slf4j   // 自动生成日志对象

public class IdempotentAspect {

    /**
     * Key解析器映射表(Key: 解析器类型,Value: 解析器实例)
     * 示例:
     * DefaultIdempotentKeyResolver.class → DefaultIdempotentKeyResolver实例
     * ExpressionIdempotentKeyResolver.class → ExpressionIdempotentKeyResolver实例
     */
    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;

    /**
     * Redis操作工具类(处理幂等Key的存储)
     */
    private final IdempotentRedisDAO idempotentRedisDAO;

    /**
     * 构造方法(依赖注入)
     * @param keyResolvers 所有Key解析器的Spring Bean集合
     * @param idempotentRedisDAO Redis操作DAO
     */
    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        // 将List转换为Map,Key是解析器的Class类型
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
        this.idempotentRedisDAO = idempotentRedisDAO;
    }

    /**
     * 环绕通知:拦截被@Idempotent注解的方法
     * @param joinPoint 切入点(包含方法、参数等信息)
     * @param idempotent 方法上的@Idempotent注解实例
     * @return 方法执行结果
     * @throws Throwable 可能抛出的异常
     *
     * 执行流程:
     * 1. 获取Key解析器 → 2. 生成唯一Key → 3. 尝试锁定 → 4. 执行业务 → 5. 异常处理
     */
    @Around(value = "@annotation(idempotent)")  // 切入带有@Idempotent注解的方法
    public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 步骤1:根据注解配置获取对应的Key解析器
        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
        // 断言确保解析器存在(找不到说明Spring容器初始化有问题)
        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");

        // 步骤2:使用解析器生成唯一Key(例如:MD5(方法签名+参数))
        String key = keyResolver.resolver(joinPoint, idempotent);

        // 步骤3:尝试在Redis中设置Key(原子性操作)
        // 参数说明:
        // key: 唯一标识
        // timeout: 过期时间(通过注解配置)
        // timeUnit: 时间单位(通过注解配置)
        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());

        // 步骤4:处理重复请求
        if (!success) {
            // 记录重复请求日志(方法签名 + 参数)
            log.info("[幂等拦截] 方法({}) 参数({}) 存在重复请求",
                    joinPoint.getSignature().toString(),
                    joinPoint.getArgs());
            // 抛出业务异常(携带注解中配置的错误提示信息)
            throw new ServiceException(
                    GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(),
                    idempotent.message());
        }

        try {
            // 步骤5:执行原始业务方法
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            // 步骤6:异常处理(参考美团GTIS设计)
            // 配置删除策略:当deleteKeyWhenException=true时,删除Key允许重试
            if (idempotent.deleteKeyWhenException()) {
                // 记录删除操作日志(实际生产可添加更详细日志)
                log.debug("[幂等异常处理] 删除Key: {}", key);
                idempotentRedisDAO.delete(key);
            }
            // 继续抛出异常(由全局异常处理器处理)
            throw throwable;
        }
    }
}

5. 实现Redis原子操作

/**
 * 幂等 Redis DAO
 *
 * @author dyh
 */
@AllArgsConstructor
public class IdempotentRedisDAO {

    /**
     * 幂等操作
     *
     * KEY 格式:idempotent:%s // 参数为 uuid
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String IDEMPOTENT = "idempotent:%s";

    private final StringRedisTemplate redisTemplate;

    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
    }

    public void delete(String key) {
        String redisKey = formatKey(key);
        redisTemplate.delete(redisKey);
    }

    private static String formatKey(String key) {
        return String.format(IDEMPOTENT, key);
    }

}

6. 自动装配


/**
 * @author dyh
 * @date 2025/4/17 18:08
 */
@AutoConfiguration(after = DyhRedisAutoConfiguration.class)
public class DyhIdempotentConfiguration {

    @Bean
    public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
        return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
    }

    @Bean
    public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
        return new IdempotentRedisDAO(stringRedisTemplate);
    }

    // ========== 各种 IdempotentKeyResolver Bean ==========

    @Bean
    public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
        return new DefaultIdempotentKeyResolver();
    }

    @Bean
    public UserIdempotentKeyResolver userIdempotentKeyResolver() {
        return new UserIdempotentKeyResolver();
    }

    @Bean
    public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
        return new ExpressionIdempotentKeyResolver();
    }

}

三、核心设计模式解析

1. 策略模式(核心设计)

应用场景:多种幂等Key生成策略的动态切换
代码体现

// 策略接口
public interface IdempotentKeyResolver {
    String resolver(JoinPoint joinPoint, Idempotent idempotent);
}

// 具体策略实现
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
    @Override
    public String resolver(...) { /* MD5(方法+参数) */ }
}

public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
    @Override
    public String resolver(...) { /* SpEL解析 */ }
}

UML图示

«interface»
IdempotentKeyResolver
+resolver(JoinPoint, Idempotent) : String
DefaultKeyResolver
ExpressionKeyResolver
UserKeyResolver

2. 代理模式(AOP实现)

应用场景:通过动态代理实现无侵入的幂等控制
代码体现

@Aspect
public class IdempotentAspect {
    @Around("@annotation(idempotent)") // 切入点表达式
    public Object around(...) {
        // 通过代理对象控制原方法执行
        return joinPoint.proceed(); 
    }
}

执行流程
客户端调用 → 代理对象拦截 → 执行幂等校验 → 调用真实方法

四、这样设计的好处

  1. 业务解耦
    • 幂等逻辑与业务代码完全分离
    • 通过注解实现声明式配置
  2. 灵活扩展
    • 新增Key策略只需实现接口
    • 支持自定义SpEL表达式
  3. 高可靠性
    • Redis原子操作防并发问题
    • 异常时自动清理Key(可配置)
  4. 性能优化
    • MD5压缩减少Redis存储压力
    • 细粒度锁控制(不同Key互不影响)
  5. 易用性
    • 开箱即用的starter组件
    • 三种内置策略覆盖主流场景

五、使用示例

@Idempotent(
    keyResolver = UserIdempotentKeyResolver.class,
    timeout = 10,
    message = "请勿重复提交订单"
)
public void createOrder(OrderDTO dto) {
    // 业务逻辑
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2339636.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue2技术练习-开发了一个宠物相关的前端静态商城网站-宠物商城网站

为了尽快学习掌握相关的前端技术&#xff0c;最近又实用 vue2做了一个宠物行业的前端静态网站商城。还是先给大家看一下相关的网站效果&#xff1a; 所以大家如果想快速的学习或者掌握一门编程语言&#xff0c;最好的方案就是通过学习了基础编程知识后&#xff0c;就开始利用…

嵌入式学习——远程终端登录和桌面访问

目录 通过桥接模式连接虚拟机和Windows系统 1、桥接模式 2、虚拟机和Windows连接&#xff08;1&#xff09; 3、虚拟机和Windows连接&#xff08;2&#xff09; 在Linux虚拟机中创建新用户 Windows系统环境下对Linux系统虚拟机操作 远程登录虚拟机&#xff08;1&#xff…

如何新建一个空分支(不继承 master 或任何提交)

一、需求分析&#xff1a; 在 Git 中&#xff0c;我们通常通过 git branch 来新建分支&#xff0c;这些分支默认都会继承当前所在分支的提交记录。但有时候我们希望新建一个“完全干净”的分支 —— 没有任何提交&#xff0c;不继承 master 或任何已有内容&#xff0c;这该怎么…

Qt编写推流程序/支持webrtc265/从此不用再转码/打开新世界的大门

一、前言 在推流领域&#xff0c;尤其是监控行业&#xff0c;现在主流设备基本上都是265格式的视频流&#xff0c;想要在网页上直接显示监控流&#xff0c;之前的方案是&#xff0c;要么转成hls&#xff0c;要么魔改支持265格式的flv&#xff0c;要么265转成264&#xff0c;如…

[第十六届蓝桥杯 JavaB 组] 真题 + 经验分享

A&#xff1a;逃离高塔(AC) 这题就是简单的签到题&#xff0c;按照题意枚举即可。需要注意的是不要忘记用long&#xff0c;用int的话会爆。 &#x1f4d6; 代码示例&#xff1a; import java.io.*; import java.util.*; public class Main {public static PrintWriter pr ne…

深⼊理解 JVM 执⾏引擎

深⼊理解 JVM 执⾏引擎 其中前端编译是在 JVM 虚拟机之外执⾏&#xff0c;所以与 JVM 虚拟机没有太⼤的关系。任何编程语⾔&#xff0c;只要能够编译出 满⾜ JVM 规范的 Class ⽂件&#xff0c;就可以提交到 JVM 虚拟机执⾏。⾄于编译的过程&#xff0c;如果你不是想要专⻔去研…

iwebsec靶场 文件包含关卡通关笔记11-ssh日志文件包含

目录 日志包含 1.构造恶意ssh登录命令 2.配置ssh日志开启 &#xff08;1&#xff09;配置sshd &#xff08;2&#xff09;配置rsyslog &#xff08;3&#xff09;重启服务 3.写入webshell木马 4.获取php信息渗透 5.蚁剑连接 日志包含 1.构造恶意ssh登录命令 ssh服务…

kafka菜鸟教程

一、kafka原理 1、kafka是一个高性能的消息队列系统&#xff0c;能够处理大规模的数据流&#xff0c;并提供低延迟的数据传输&#xff0c;它能够以每秒数十万条消息的速度进行读写操作。 二、kafka优点 1、服务解耦 &#xff08;1&#xff09;提高系统的可维护性‌ 通过服务…

应用镜像是什么?轻量应用服务器的镜像大全

应用镜像是轻量应用服务器专属的&#xff0c;镜像就是轻量应用服务器的装机盘&#xff0c;应用镜像在原有的纯净版操作系统上集成了应用程序&#xff0c;例如WordPress应用镜像、宝塔面板应用镜像、WooCommerce等应用&#xff0c;阿里云服务器网aliyunfuwuqi.com整理什么是轻量…

深入理解分布式缓存 以及Redis 实现缓存更新通知方案

一、分布式缓存简介 1. 什么是分布式缓存 分布式缓存&#xff1a;指将应用系统和缓存组件进行分离的缓存机制&#xff0c;这样多个应用系统就可以共享一套缓存数据了&#xff0c;它的特点是共享缓存服务和可集群部署&#xff0c;为缓存系统提供了高可用的运行环境&#xff0c…

Spring Boot 中的自动配置原理

2025/4/6 向全栈工程师迈进&#xff01; 一、自动配置 所谓的自动配置原理就是遵循约定大约配置的原则&#xff0c;在boot工程程序启动后&#xff0c;起步依赖中的一些bean对象会自动的注入到IOC容器中。 在讲解Spring Boot 中bean对象的管理的时候&#xff0c;我们注入bean对…

剑指Offer(数据结构与算法面试题精讲)C++版——day16

剑指Offer&#xff08;数据结构与算法面试题精讲&#xff09;C版——day16 题目一&#xff1a;序列化和反序列化二叉树题目二&#xff1a;从根节点到叶节点的路径数字之和题目三&#xff1a;向下的路径节点值之和附录&#xff1a;源码gitee仓库 题目一&#xff1a;序列化和反序…

windows server C# IIS部署

1、添加IIS功能 windows server 2012、windows server 2016、windows server 2019 说明&#xff1a;自带的是.net 4.5 不需要安装.net 3.5 尽量使用 windows server 2019、2016高版本&#xff0c;低版本会出现需要打补丁的问题 2、打开IIS 3、打开iis应用池 .net 4.5 4、添…

【教程】PyTorch多机多卡分布式训练的参数说明 | 附通用启动脚本

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 torchrun 一、什么是 torchrun 二、torchrun 的核心参数讲解 三、torchrun 会自动设置的环境变量 四、torchrun 启动过程举例 机器 A&#…

Neo4j初解

Neo4j 是目前应用非常广泛的一款高性能的 NoSQL 图数据库&#xff0c;其设计和实现专门用于存储、查询和遍历由节点&#xff08;实体&#xff09;、关系&#xff08;边&#xff09;以及属性&#xff08;键值对&#xff09;构成的图形数据模型。它的核心优势在于能够以一种自然且…

音视频小白系统入门课-2

本系列笔记为博主学习李超老师课程的课堂笔记&#xff0c;仅供参阅 课程传送门&#xff1a;音视频小白系统入门课 音视频基础ffmpeg原理 往期课程笔记传送门&#xff1a; 音视频小白系统入门笔记-0音视频小白系统入门笔记-1 课程实践代码仓库&#xff1a;传送门 音视频编解…

Linux:安装 CentOS 7(完整教程)

文章目录 一、简介二、安装 CentOS 72.1 虚拟机配置2.2 安装CentOS 7 三、结语 一、简介 CentOS&#xff08;Community ENTerprise Operating System&#xff09;是一个基于 Linux 的发行版之一&#xff0c;旨在提供一个免费的、企业级的计算平台&#xff0c;因其稳定性、安全…

MATLAB 控制系统设计与仿真 - 34

多变量系统知识回顾 - MIMO system 这一章对深入理解多变量系统以及鲁棒分析至关重要 首先&#xff0c;对于如下系统&#xff1a; 当G(s)为单输入&#xff0c;单输出系统时&#xff1a; 如果&#xff1a; 则&#xff1a; 所以 因此&#xff0c;对于SISO&#xff0c;系统的增…

【网络】通过Samba实现Window挂在Linux服务器路径

有时候我们去进行内网部署时&#xff0c;会遇到客户或者甲方爸爸说&#xff0c;需要将Linux中的某个路径共享出去到Window上&#xff0c;挂载出比如Z:\这种盘符。通过打开Z盘&#xff0c;来查看服务器的指定目录下的数据。 步骤1&#xff1a; 在Linux中安装samba yum install…

架构思维:缓存层场景实战_读缓存(下)

文章目录 Pre业务场景缓存存储数据的时机与常见问题解决方案1. 缓存读取与存储逻辑2. 高并发下的缓存问题及解决方案3. 缓存预热&#xff08;减少冷启动问题&#xff09; 缓存更新策略&#xff08;双写问题&#xff09;1. 先更新缓存&#xff0c;再更新数据库&#xff08;不推荐…