万字长文!15个最佳实践讲透接口设计的陷阱 !零基础入门到精通,收藏这一篇就够了

819 篇文章 36 订阅
308 篇文章 12 订阅

推荐一个原创技术号-非科班大厂码农,号主是机械专业转行进入腾讯的后端程序员!

我们在设计接口时,需要全面考虑多个关键方面,以确保接口既实用又安全,能够有效地满足系统的需求。以下是设计接口时需要重点关注的几个方面:

  • 安全性:接口设计必须考虑安全性,确保接口能够有效防止数据被恶意攻击或泄露。保障数据安全性的措施包括对输入进行验证、实施适当的权限控制、加密传输等手段。

  • 可扩展性:接口应具备良好的可扩展性,像一个弹性容器,能够随着需求的变化灵活扩展功能。设计时应考虑未来可能的功能增加或修改,避免因需求变化而频繁修改接口。

  • 稳定性:接口的稳定性至关重要,就像稳定的电源一样,为系统的正常运行提供可靠的保障。设计稳定的接口可以避免频繁的变化导致系统不稳定,确保接口在长时间内保持一致的行为。

本文将详细整理接口设计中应当注意的一些重点,以帮助您在设计接口时做出全面而周到的考虑。

1. 接口参数验证

输入参数的合法性验证是设计良好接口的基础,通过有效的参数验证,系统可以过滤掉许多无效请求,从而提高系统的稳定性和可靠性。输入参数的验证通常可以分为两类:常规验证和业务验证。

常规验证:这类验证通常适用于所有接口,无论业务场景如何,都需要进行的基本检查。常规验证主要包括以下几个方面:

  • Token验证:确保请求者具有访问接口的权限,防止未授权访问。

  • 必填字段验证:检查请求中是否包含所有必要的字段,防止由于缺少必需信息而导致的错误。

  • 长度验证:验证输入数据的长度是否符合要求,例如用户名或密码长度。

  • 类型验证:确保输入数据的类型符合预期,例如整数、字符串、日期等。

业务验证:这类验证是针对特定业务场景的,确保请求的合法性符合业务规则。例如在电商业务中:

  • 订单金额验证:在商品下单接口中,验证订单金额是否大于0,防止无效的订单请求。

  • 库存验证:检查商品库存是否充足,防止超卖情况发生。

  • 用户状态验证:验证用户是否满足某些业务条件,例如账户是否被禁用或是否有足够权限进行某项操作。

public  String  placeOrder ( @RequestBody Order order ) {        if (order.getOrderAmount ( ) > 0 ) {            return  "订单下单成功。" ;        } else {            return  "订单金额必须大于零。" ;        }    }   

通过接口参数验证,可以让接口可以在不增加复杂性的情况下,有效防止潜在错误,提高整体系统的可靠性。

2. 关键接口日志打印

关键接口的日志记录至关重要,它可以帮助我们快速定位和解决问题。通常,以下几个地方需要记录日志:

  • 输入参数日志:记录每个请求的输入参数。这可以帮助我们了解客户端发送的请求数据,并验证是否符合预期。

  • 输出参数日志:记录接口响应的输出参数。这有助于确认接口的返回数据是否正确,以及系统在处理请求时的行为。

  • 异常日志:记录接口在处理请求过程中发生的异常。这有助于追踪和修复潜在的错误,确保系统的稳定性和可靠性。

通过记录这些日志,我们可以在出现问题时,通过查看日志快速定位问题的根源。例如,当客户报告问题时,我们可以通过日志中的“唯一标识ID”来查询这次请求的完整链路,从而有效地排查和解决问题。此外,如果问题不是由我们的接口引起的,日志也可以作为证据,避免不必要的争论。

如果日志量过大,我们可以通过设置日志级别来控制日志的输出。只有在特定情况下,如调试阶段或问题排查时,才打印详细日志,从而避免日志泛滥。

@Getter    class  InputData {        private String id;        private  int param;           public  InputData ( int param) {            this .id = UUID.randomUUID().toString();            this .param = param;        }    }       public  class  InterfaceLoggerExample {           private  static  final  Logger  logger  = LoggerFactory.getLogger(InterfaceLoggerExample.class);           public  static  void  main (String[] args) {            InputData  inputData  =  new  InputData ( 10 );               try {                // 使用唯一标识符记录输入参数               logger.info( "接口输入参数(ID:{}):{}" , inputData.getId(), inputData.getParam());                   // 模拟接口处理               int  result  = processInput(inputData.getParam());                   // 使用相同标识符记录输出参数               logger.info( "接口输出参数(ID:{}):{}" , inputData.getId(), result);            } catch (Exception e) {                // 当接口发生错误时,记录错误消息               logger.error( "接口发生错误(ID:{}): " , inputData.getId(), e);            }        }           private  static  int  processInput ( int input) {            if (input % 2 == 0 ) {                return input * 2 ;            } else {                throw  new  IllegalArgumentException ( "输入不是偶数" );            }        }    }   

3. 接口幂等性设计

所谓幂等性,是指多次调用某个方法或接口不会改变业务状态,并且能确保重复调用的结果与单次调用的结果是一致的。简而言之,幂等性确保了无论方法或接口被调用多少次,其结果都是相同的,不会对系统产生意外影响。

在实际开发中,常见的操作包括创建(Create)、读取(Read)、更新(Update)和删除(Delete),即CRUD操作。在这些操作中:

  • “读”操作(Read)和“删除”操作(Delete)通常是天然幂等的。例如,读取数据不会修改系统状态,而删除某个资源多次也只会删除一次(如果资源已经不存在,则后续的删除操作不会产生影响)。

  • “创建”操作(Create)本质上是非幂等的,因为每次创建都会生成新的数据。如果重复调用创建操作,会产生多个不同的数据项。

  • “更新”操作(Update)则可能是幂等的,也可能是非幂等的。这取决于具体的业务场景。如果更新操作是将数据更改为某个固定值,无论执行多少次,结果都是一致的,那么该操作就是幂等的。相反,如果更新操作是基于当前数据状态的变化(例如,每次更新都增加计数器的值),则该操作可能是非幂等的。

有关幂等性设计和实现的更多详细内容,可以参考我的另一篇文章。

4. 接口限流控制

限流是为了保证系统的稳定性,特别是当我们提供接口给第三方系统使用时,实施限流措施显得尤为重要。

通过限流,我们可以有效防止接口被恶意刷量,从而避免对服务层造成不必要的压力。此外,限流还能够防止接口被滥用,确保资源的公平使用。

下面是一个使用自定义注解和 AOP 实现的限流案例。

/**    * @Target表示该注解可以应用于方法。   * @Retention表示该注解在运行时可用。   */    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.RUNTIME)    public  @interface RateLimiter {        /**         * 默认值为 1。        */        int  value ()  default  1 ;        /**         * 默认值为 1 秒。        */        int  durationInSeconds ()  default  1 ;    }       /**    * 用于速率限制的 Aspect 类。   */    @Aspect    @Component    public  class  RateLimiterAspect {           /**         * 一个并发哈希map,用于存储不同方法的速率限制器。        */        private  final ConcurrentHashMap<String, RateLimiter> rateLimiters = new  ConcurrentHashMap <>();           /** * 切入点表达式,用于指向用@RateLimiter        注解的目标方法。     */           @Pointcut("@annotation(RateLimiter)")        public  void  rateLimiterPointcut (RateLimiter rateLimiterAnnotation) {        }           /**         * 围绕目标方法进行速率限制的通知方法。        */        @Around("rateLimiterPointcut(rateLimiterAnnotation)")        public Object around (ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation)  throws Throwable {            int  permits  = rateLimiterAnnotation.value();            int  durationInSeconds  = rateLimiterAnnotation.durationInSeconds();               // 使用方法签名作为速率限制器的key。           String  key  = joinPoint.getSignature().toLongString();            com.google.common.util.concurrent. RateLimiter  rateLimiter  = rateLimiters.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create(( double ) permits / durationInSeconds));               // 尝试获取令牌。如果成功,则执行该方法。否则,抛出异常。           if (rateLimiter.tryAcquire()) {                return joinPoint.proceed();            } else {                throw  new RuntimeException ( "超出速率限制。" );            }        }    }       /**    * REST 控制器类。   */    @RestController    public  class  ApiController {           /**         * 速率限制设置为每分钟 10 个请求的端点。        */        @GetMapping("/api/limited")        @RateLimiter(value = 10, durationInSeconds = 60)        public String limitedEndpoint () {            return  "此 API 的速率限制为每分钟 10 个请求。" ;        }           /**         * 未设置速率限制的端点。        */        @GetMapping("/api/unlimited")        public String unlimitedEndpoint () {            return  "此 API 没有速率限制。" ;        }    }   

5. 敏感数据屏蔽

在接口调用过程中,可能会涉及一些敏感字段,如“身份证号”、“银行卡号”、“住址”、“手机号”等。为了保护用户的隐私和数据安全,这些敏感数据通常需要进行屏蔽处理。例如,将“123-456-7890”屏蔽为“123-456-XXX”。

这种处理方式可以有效防止敏感信息的泄露,同时仍然保留数据的部分信息以供验证或处理。

6. 请求接口前提条件——Token

大致流程如下:首先,用户成功登录后,系统会生成一个 token 并返回给前端。与此同时,后端会将这个 token 作为 key,将用户信息作为 value 存入 Redis 缓存中。

之后,用户在访问其他接口时,需要将这个 token 放在请求头中发送给后端。后端通过拦截器拦截请求,检查 Redis 中是否存在对应的 token。如果 token 不存在,则返回提示,告知用户未登录。

当然,有些接口如注册接口不需要用户登录即可访问。为此,拦截器可以配置为过滤这些不需要登录的接口,从而避免不必要的验证过程。

7. 调用第三方接口时考虑异常、超时和重试

在调用第三方接口时,必须考虑以下几点:

  • 超时设置:由于无法确定第三方接口的响应时间,应该为调用设置一个合理的超时时间,以避免接口卡顿影响系统性能。
public class TimeoutExample {       public static void main(String[] args) {           try {               URL url = new URL("https://payment.example.com/process");               HttpURLConnection connection = (HttpURLConnection) url.openConnection();               connection.setConnectTimeout(5000); // 设置连接超时为 5 秒               connection.setReadTimeout(5000);    // 设置读取超时为 5 秒               connection.setRequestMethod("GET");                  int responseCode = connection.getResponseCode();               if (responseCode == 200) {                   // 处理支付成功逻辑                   System.out.println("支付成功");               } else {                   // 处理支付失败逻辑                   System.out.println("支付失败,状态码:" + responseCode);               }           } catch (Exception e) {               System.out.println("支付请求超时或失败:" + e.getMessage());           }       }   }   
  • 异常处理:接口调用过程中可能出现异常。无论异常类型如何,都应该记录详细的日志,以便后续排查问题。同时,需要考虑在异常发生时是否进行重试或触发报警机制。
public class ExceptionHandlingExample {       private static final Logger logger = Logger.getLogger(ExceptionHandlingExample.class.getName());          public static void main(String[] args) {           try {               URL url = new URL("https://api.weather.com/v3/weather");               HttpURLConnection connection = (HttpURLConnection) url.openConnection();               connection.setRequestMethod("GET");                  int responseCode = connection.getResponseCode();               if (responseCode == 200) {                   try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {                       String inputLine;                       StringBuilder content = new StringBuilder();                       while ((inputLine = in.readLine()) != null) {                           content.append(inputLine);                       }                       System.out.println("天气信息:" + content.toString());                   }               } else {                   logger.severe("API返回错误,状态码:" + responseCode);               }           } catch (IOException e) {               logger.severe("调用天气API失败:" + e.getMessage());               // 触发报警或进一步处理           }       }   }   
  • 重试策略:如果调用第三方接口失败,可能是由于网络问题或临时故障。需要设计合理的重试策略,包括重试次数和时间间隔,以增加成功的可能性。
public class RetryStrategyExample {       public static void main(String[] args) {           int maxRetries = 3; // 最大重试次数           int delay = 2000;   // 重试间隔时间(毫秒)              for (int attempt = 1; attempt <= maxRetries; attempt++) {               try {                   URL url = new URL("https://inventory.example.com/api/sync");                   HttpURLConnection connection = (HttpURLConnection) url.openConnection();                   connection.setRequestMethod("GET");                      int responseCode = connection.getResponseCode();                   if (responseCode == 200) {                       System.out.println("库存同步成功!");                       break; // 如果成功,退出重试循环                   } else {                       System.out.println("库存同步失败,状态码:" + responseCode);                   }               } catch (Exception e) {                   System.out.println("第 " + attempt + " 次重试失败,错误:" + e.getMessage());                   if (attempt == maxRetries) {                       System.out.println("所有重试均失败,请检查网络或联系支持。");                   }               }                  try {                   Thread.sleep(delay); // 等待一段时间再进行下一次重试               } catch (InterruptedException ie) {                   Thread.currentThread().interrupt();                   System.out.println("重试等待被中断。");                   break;               }           }       }   }   

8. 统一响应数据格式

在接口设计中,统一的响应数据格式是非常重要的。常见的响应体结构包括:

  • 状态码 (code):用于指示接口请求的结果。状态码帮助客户端快速判断请求是否成功。

  • 信息描述 (message):提供关于请求结果的详细信息,例如错误原因或成功的描述。

  • 响应数据 (data):包含实际的业务数据。成功时返回请求的数据,失败时可能为空或包含错误信息。

统一的响应格式使客户端可以根据状态码快速处理接口请求的结果,成功时处理数据,失败时直接处理错误信息。

以下是一个示例,展示如何创建统一的响应数据格式类 ApiResponse 并使用它来处理 RESTful API 的响应结果:

public class ApiResponse<T> {       private int code;       // 状态码       private String message; // 信息描述       private T data;         // 响应数据          public ApiResponse(int code, String message, T data) {           this.code = code;           this.message = message;           this.data = data;       }          // 快速构建成功响应       public static <T> ApiResponse<T> success(T data) {           return new ApiResponse<>(200, "success", data);       }          // 快速构建失败响应       public static <T> ApiResponse<T> error(int code, String message) {           return new ApiResponse<>(code, message, null);       }          // Getters 和 Setters       public int getCode() {           return code;       }          public void setCode(int code) {           this.code = code;       }          public String getMessage() {           return message;       }          public void setMessage(String message) {           this.message = message;       }          public T getData() {           return data;       }          public void setData(T data) {           this.data = data;       }   }   

假设我们有一个简单的用户服务 API,它根据用户 ID 获取用户信息。如果成功,返回用户数据;如果失败,返回相应的错误信息。

RestController   @RequestMapping("/api/users")   public class UserController {          @GetMapping("/{id}")       public ApiResponse<User> getUserById(@PathVariable("id") Long id) {           // 模拟获取用户数据的逻辑           User user = findUserById(id); // 假设这个方法会从数据库中获取用户数据              if (user != null) {               // 返回成功响应               return ApiResponse.success(user);           } else {               // 返回失败响应               return ApiResponse.error(404, "not find");           }       }          // 模拟获取用户数据的实现       private User findUserById(Long id) {           // 伪代码:模拟数据库查询逻辑           if (id == 1L) {               return new User(1L, "Alice", "alice@example.com");           } else {               return null; // 如果用户ID不匹配,返回null表示用户不存在           }       }   }   

响应格式:

#成功   {     "code": 200,     "message": "success",     "data": {       "id": 1,       "name": "Alice",       "email": "alice@example.com"     }   }      #失败   {     "code": 404,     "message": "not find",     "data": null   }   

9. 接口的单一职责

单一职责原则强调,每个接口应专注于完成一个明确的功能,即一个接口只做一件事,而不是同时处理多个任务。

遵循单一职责原则有很多好处。首先,它使代码更容易维护。当一个接口只负责一个功能时,修改该功能时只需调整相应的接口,不会影响到其他无关的功能。其次,这种设计便于扩展。如果需要新增功能,可以创建新的接口,而不会干扰现有接口的正常运作。最后,代码的可读性也会大大提高。每个接口的名称和功能清晰明了,其他开发人员可以轻松理解其用途。

例如,在一个购物系统中,可以设计多个接口:一个订单接口专门处理订单相关的操作,一个商品接口专门负责商品管理,还有一个用户接口专注于用户管理。通过这种方式,各个接口的职责分明,系统变得更加稳定且易于维护。

//订单接口负责所有订单相关的操作,例如创建订单、取消订单和查看订单详情。   public interface OrderService {       void createOrder(int userId, int productId, int quantity);       void cancelOrder(int orderId);       Order getOrderDetails(int orderId);   }      
//商品接口负责所有商品管理的操作,例如添加商品、更新商品信息和获取商品详情。   public interface ProductService {       void addProduct(Product product);       void updateProduct(int productId, Product product);       Product getProductDetails(int productId);   }      
//用户接口负责所有用户管理的操作,例如注册用户、更新用户信息和获取用户详情。   public interface UserService {       void registerUser(User user);       void updateUser(int userId, User user);       User getUserDetails(int userId);   }      

10. 接口是否需要采用异步处理

在接口设计中,有些操作更适合采用异步处理。比如你实现了一个用户注册接口,当用户注册成功后,我们可能需要发送邮件或短信通知用户。由于通知的失败不会影响用户注册的成功,因此这些通知操作适合使用异步处理。

那么如何进行异步操作呢?一种常见的异步处理方法是使用消息队列。

具体做法是:用户注册成功后,系统作为消息生产者,生成一条注册成功的消息并发送到消息队列。随后,消费者从队列中拉取到这条消息,负责执行发送通知的操作(例如发送邮件或短信)。这种设计能有效解耦业务逻辑,提升系统的性能和可靠性。

下面是一个基于Java的示例,演示如何使用消息队列来处理用户注册成功后的异步通知操作。这个例子假设使用了分布式消息队列(如Kafka ),模拟用户注册后将消息发送到队列中,然后由消费者监听队列并处理通知操作。

生产者代码:用户注册成功后,发送消息到队列

首先,我们需要在用户注册成功时,将相关消息发送到 Kafka 中。我们可以使用 Kafka 的生产者客户端来完成这一步。

public class UserRegistrationService {          private KafkaProducer<String, String> producer;       private static final String TOPIC = "user-registration";          public UserRegistrationService() {           Properties props = new Properties();           props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");           props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());           props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());              producer = new KafkaProducer<>(props);       }          public void registerUser(String userId, String email) {           // 注册用户逻辑           // ...              // 用户注册成功后,发送消息到 Kafka           String message = "User registered successfully: " + userId + ", Email: " + email;           ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, userId, message);              producer.send(record, (RecordMetadata metadata, Exception exception) -> {               if (exception != null) {                   exception.printStackTrace();               } else {                   System.out.println("Message sent to topic " + metadata.topic());               }           });       }          public void close() {           producer.close();       }   }   
消费者从 Kafka 中拉取消息并处理通知
public class NotificationService {          private KafkaConsumer<String, String> consumer;       private static final String TOPIC = "user-registration";          public NotificationService() {           Properties props = new Properties();           props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");           props.put(ConsumerConfig.GROUP_ID_CONFIG, "notification-service");           props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());           props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());           props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");              consumer = new KafkaConsumer<>(props);           consumer.subscribe(Collections.singletonList(TOPIC));       }          public void start() {           while (true) {               var records = consumer.poll(Duration.ofMillis(100));               for (ConsumerRecord<String, String> record : records) {                   // 处理通知逻辑                   System.out.println("Received message: " + record.value());                   // 在这里发送邮件或短信                   sendNotification(record.value());               }           }       }          private void sendNotification(String message) {           // 实现邮件或短信发送逻辑           System.out.println("Sending notification: " + message);       }          public void close() {           consumer.close();       }   }   

11. 核心接口的线程池隔离

在业务代码中,线程池是一种常用的工具,它不仅用于核心接口,也经常在普通接口中被用来提高处理效率。然而,在线程池的使用中,合理的隔离管理是非常重要的,尤其是在核心接口与普通接口之间。

如果没有对线程池进行合理的隔离管理,普通接口中的潜在问题可能会影响到主业务的正常运行。

例如,假设一个普通接口在使用线程池时出现了 bug,导致线程池被占满。这种情况可能会阻碍主业务接口的执行,从而影响整个系统的稳定性。为了避免这种风险,我们需要对线程池进行合理的隔离管理,以确保主业务不会受到非核心业务故障的影响。通过这种方式,我们可以提高系统的稳定性和可靠性。

public class ThreadPoolIsolationExample {          // 核心业务线程池       private static final ExecutorService coreBusinessPool = Executors.newFixedThreadPool(10);              // 普通业务线程池       private static final ExecutorService secondaryBusinessPool = Executors.newFixedThreadPool(5);          public static void main(String[] args) {           // 提交核心业务任务           Future<String> coreTaskFuture = coreBusinessPool.submit(new CoreBusinessTask());              // 提交普通业务任务           Future<String> secondaryTaskFuture = secondaryBusinessPool.submit(new SecondaryBusinessTask());              try {               // 获取核心业务任务的结果               String coreTaskResult = coreTaskFuture.get();               System.out.println("Core business task result: " + coreTaskResult);                  // 获取普通业务任务的结果               String secondaryTaskResult = secondaryTaskFuture.get();               System.out.println("Secondary business task result: " + secondaryTaskResult);              } catch (Exception e) {               e.printStackTrace();           } finally {               // 关闭线程池               coreBusinessPool.shutdown();               secondaryBusinessPool.shutdown();           }       }          // 核心业务任务       static class CoreBusinessTask implements Callable<String> {           @Override           public String call() throws Exception {               // 模拟核心业务处理               Thread.sleep(2000);               return "Core Business Process Completed";           }       }          // 普通业务任务       static class SecondaryBusinessTask implements Callable<String> {           @Override           public String call() throws Exception {               // 模拟普通业务处理               Thread.sleep(2000);               return "Secondary Business Process Completed";           }       }   }   

12. 提高接口响应时间

  1. 数据库查询尽量使用索引,以优化查询速度。

  2. 考虑是否添加缓存:本地缓存、Redis缓存、ES存储等。

13. 优化接口串行处理为并行处理

在开发网站首页时,我们通常需要查询多个数据源,如用户信息、头部信息和新闻信息等。最简单的方法是串行地逐个调用接口,但这种方式可能会导致较长的响应时间。

为了提升效率,我们可以采用并行调用的方式,同时查询所有必要的数据,以避免阻塞。

CompletableFuture 提供了一种更现代和灵活的方式来处理并发任务。它允许我们异步地执行任务并在所有任务完成后聚合结果。以下是如何使用 CompletableFuture 来查询首页数据的示例:

Map < Long,List < SubjectLabelBO >> map = new HashMap <>();    List < CompletableFuture < Map < Long,List < SubjectLabelBO >>>> completableFutureList =     categoryBOList.stream().map(category ->            CompletableFuture .supplyAsync(() -> getLabelBOList(category), labelThreadPool)    ).collect( Collectors .toList());       completableFutureList.forEach(future -> {        try {            Map < Long,List < SubjectLabelBO >> resultMap = future.get();            map.putAll(resultMap);        } catch ( Exception e) {            e.printStackTrace();        }    });   

14. 控制接口的锁粒度

在高并发场景中,我们经常需要对共享资源进行加锁操作,以确保线程安全。然而,加锁时需要注意锁的粒度问题。如果加锁粒度过大,会导致不必要的性能损耗。

什么是锁的粒度? 锁的粒度指的是锁定资源的范围程度。假设你带了一封情书回家,但不想让父母发现,你可以选择将其放在一个可以上锁的抽屉里,而不是锁上整个房间。这种做法就像是降低了锁的粒度:你只锁定了小范围的抽屉,而不是整个房间,从而减少了对其他活动的干扰。

代码示例:

锁粒度过大

把方法A和方法B全部锁住,但实际上我只想锁住方法A,这就是锁粒度过大。

void test(){        synchronized (this) {           B ();           A ();        }    }   
降低锁粒度

只锁真正需要的 A (),这样就降低了锁的粒度,提升了高并发场景下的接口性能。

void test (){        B ();       synchronized (this) {           A ();        }    }   

15. 避免长事务

在长事务过程中,CPU和内存占用可能会升高,严重时会导致服务器整体响应缓慢,甚至使线上应用无法使用。长事务产生的原因可能由 SQL 查询本身引起,也可能与应用层的事务控制逻辑密切相关。

为了尽可能避免长事务问题,我们可以采取以下措施:

  1. 避免在事务中调用远程接口:远程接口调用通常涉及网络延迟和不确定的响应时间,将其放在事务内部可能会导致事务时间延长,从而增加系统的负担。尽量将这些操作移到事务之外,以缩短事务的持续时间。

  2. 将查询操作移出事务:如果可能,将一些查询相关的操作放在事务之外。这样可以减少事务的持续时间,避免因长时间持有锁而影响系统性能。

  3. 并发场景下,尽量避免使用@Transactional注解操作事务,使用 TransactionTemplate 可以更精细地控制事务的范围和行为,从而提高系统的灵活性和性能。

在传统的事务管理中,我们使用 @Transactional 注解来自动处理事务。这种方式虽然简单方便,但在复杂的事务场景下,可能不够灵活或精准。

@Transactional    public int createUser(User user){        // 保存用户信息       userDao .save (user);        passCertDao .updateFlag (user.getPassId());        // 此方法是远程接口调用       sendEmailRpc (user.getEmail());        return user .getUserId ();    }   

为了更精细地控制事务的范围和行为,我们可以使用 TransactionTemplate,它允许我们精确规划事务的边界。使用 TransactionTemplate 的示例如下:

@Resource    private TransactionTemplate transactionTemplate;       public int createUser (User user){        transactionTemplate .execute (transactionStatus -> {          try {             userDao.save(user);             passCertDao .updateFlag (user.getPassId());          } catch (Exception e) {             // 异常手动设置回滚            transactionStatus .setRollbackOnly ();          }          return true;        });        // 此方法是远程接口调用       sendEmailRpc (user.getEmail());        return user .getUserId ();    }

为了帮助大家更好的学习网络安全,我给大家准备了一份网络安全入门/进阶学习资料,里面的内容都是适合零基础小白的笔记和资料,不懂编程也能听懂、看懂这些资料!

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

[2024最新CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]


在这里插入图片描述

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

[2024最新CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]
在这里插入图片描述

在这里插入图片描述

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

[2024最新CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值