推荐一个原创技术号-非科班大厂码农,号主是机械专业转行进入腾讯的后端程序员!
我们在设计接口时,需要全面考虑多个关键方面,以确保接口既实用又安全,能够有效地满足系统的需求。以下是设计接口时需要重点关注的几个方面:
-
安全性:接口设计必须考虑安全性,确保接口能够有效防止数据被恶意攻击或泄露。保障数据安全性的措施包括对输入进行验证、实施适当的权限控制、加密传输等手段。
-
可扩展性:接口应具备良好的可扩展性,像一个弹性容器,能够随着需求的变化灵活扩展功能。设计时应考虑未来可能的功能增加或修改,避免因需求变化而频繁修改接口。
-
稳定性:接口的稳定性至关重要,就像稳定的电源一样,为系统的正常运行提供可靠的保障。设计稳定的接口可以避免频繁的变化导致系统不稳定,确保接口在长时间内保持一致的行为。
本文将详细整理接口设计中应当注意的一些重点,以帮助您在设计接口时做出全面而周到的考虑。
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. 提高接口响应时间
-
数据库查询尽量使用索引,以优化查询速度。
-
考虑是否添加缓存:本地缓存、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 查询本身引起,也可能与应用层的事务控制逻辑密切相关。
为了尽可能避免长事务问题,我们可以采取以下措施:
-
避免在事务中调用远程接口:远程接口调用通常涉及网络延迟和不确定的响应时间,将其放在事务内部可能会导致事务时间延长,从而增加系统的负担。尽量将这些操作移到事务之外,以缩短事务的持续时间。
-
将查询操作移出事务:如果可能,将一些查询相关的操作放在事务之外。这样可以减少事务的持续时间,避免因长时间持有锁而影响系统性能。
-
并发场景下,尽量避免使用@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大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取