记录些Spring+题集(10)

跨域问题解决

跨域资源共享(CORS,Cross-Origin Resource Sharing)是一个浏览器安全特性,它允许网页从一个域请求到另一个域的资源。在开发基于微服务的应用程序时,跨域请求是一个常见的问题。本文将详细介绍几种在Spring Boot 3中解决跨域问题的方法,并提供相应的代码示例。

1. 什么是跨域

跨域问题出现在浏览器试图访问不同源(域、协议、端口)的资源时。为了安全,浏览器默认阻止这种访问。这种限制对前后端分离开发造成了一定的困扰,因此需要通过某些方式来允许跨域请求。

2. Spring Boot 3解决跨域问题的方法概述

Spring Boot提供了多种方法来解决跨域问题,主要包括:

  • 使用@CrossOrigin注解

  • 全局配置跨域

  • 使用过滤器(Filter)

  • 使用CORS配置类

3. 使用@CrossOrigin注解解决跨域问题

@CrossOrigin注解可以直接在控制器或者方法上使用,允许特定的域名访问。

@RestController@RequestMapping("/api")public class ExampleController {
    @CrossOrigin(origins = "http://example.com")    @GetMapping("/greeting")    public ResponseEntity<String> greeting() {        return ResponseEntity.ok("Hello, World!");    }}

上述代码允许http://example.com域名访问/api/greeting接口。

4. 全局配置跨域

如果需要为所有控制器统一配置跨域,可以通过实现一个WebMvcConfigurer接口来达到目的。

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configurationpublic class WebConfig implements WebMvcConfigurer {
    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/**")                .allowedOrigins("http://example.com")                .allowedMethods("GET", "POST", "PUT", "DELETE")                .allowedHeaders("*")                .allowCredentials(true);    }}

此配置允许http://example.com域名通过GET、POST、PUT和DELETE方法访问所有路径。

5. 使用过滤器解决跨域问题

使用过滤器可以在Spring Security中配置跨域请求。这种方法适用于需要更复杂的安全配置场景。

import org.springframework.context.annotation.Configuration;import org.springframework.web.filter.CorsFilter;import org.springframework.context.annotation.Bean;import org.springframework.core.Ordered;import org.springframework.core.annotation.Order;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configurationpublic class CorsConfig {
    @Bean    @Order(Ordered.HIGHEST_PRECEDENCE)    public CorsFilter corsFilter() {        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();        CorsConfiguration config = new CorsConfiguration();        config.setAllowCredentials(true);        config.addAllowedOrigin("http://example.com");        config.addAllowedHeader("*");        config.addAllowedMethod("*");        source.registerCorsConfiguration("/**", config);        return new CorsFilter(source);    }}

此配置允许http://example.com域名通过任意HTTP方法访问所有路径。

6. 使用CORS配置类解决跨域问题

Spring Security 5提供了一种新的跨域配置方法,可以在安全配置中直接定义CORS规则。

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.web.SecurityFilterChain;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import org.springframework.web.filter.CorsFilter;
@EnableWebSecurity@Configurationpublic class SecurityConfig {
    @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {        http.cors().and().csrf().disable();        return http.build();    }
    @Bean    public CorsFilter corsFilter() {        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();        CorsConfiguration config = new CorsConfiguration();        config.setAllowCredentials(true);        config.addAllowedOrigin("http://example.com");        config.addAllowedHeader("*");        config.addAllowedMethod("*");        source.registerCorsConfiguration("/**", config);        return new CorsFilter(source);    }}

在此配置中,我们首先禁用了CSRF保护(仅在确实需要时),然后配置了CORS过滤器。

7. 总结

在Spring Boot 3中,有多种方法可以解决跨域问题,包括使用@CrossOrigin注解、全局配置、过滤器和CORS配置类等。

如何设计一个秒杀系统?

1. 满足高并发,快速响应

秒杀系统的瞬时流量特别大,为了防止系统被打垮了,一般要做压测。测清楚你的系统支撑的最大并发是多少,确定系统的瓶颈点,让自己心里有底,最好预防措施。

压测完要分析整个调用链路,性能可能出现问题是网络层(如带宽)、Nginx层、服务层、还是数据路缓存等中间件等等。

要快速响应,前期还可以做一些优化。比如页面资源静态化、缓存热数据预加载、CDN加速等等。

2. 防止超卖

做秒杀系统,最主要就是防止超卖问题。所以是要加分布式锁的。一般都是用Redis分布式锁。

但是呢,使用redis分布式锁,可能会有坑的,否则也是可能导致超卖问题。比如要注意这些常见redis分布式锁的坑:

  • 非原子操作(setnx + expire)

  • 被别的客户端请求覆盖( setnx + value为过期时间)

  • 忘记设置过期时间

  • 业务处理完,忘记释放锁

  • B的锁被A给释放了

  • 释放锁时,不是原子性

  • 锁过期释放,业务没执行完

  • Redis分布式锁和@transactional一起使用失效

  • Redis主从复制导致的坑

Redis分布式锁的10个坑

3. 防止恶意刷子

一般秒杀系统,都是大多数人去抢比较有价值的商品。有些黄牛可能会写脚本模拟发大批量请求,从而导致真正的用户抢不到。

这时候,一般需要加入黑名单或者限流处理,比如限制用户多次请求,或者限制IP等等。比如一个用户最多只能请求5次,一个IP最大请求10次等等。

4. 页面静态化

秒杀流程是这样的:

图片

如果所有的前端请求,都打到服务端,服务端压力比较大。实际上,一些请求只是访问比如商品名称、商品图片等资源。这些请求没必要跟后端交互,可以做页面静态化。也就是说,对页面缓存,用户请求URL的时候,请求不再打到服务端,直接找到静态服务资源即可。

图片

如果用户在全国各地的话,单纯把页面静态化还不是最优解。还可以将静态资源(如图片、CSS、JS)分发到 CDN 节点,它通过就近分发原则,可以提高资源的响应速率。

CDN(Content Delivery Network,内容分发网络)是一种通过在全球范围内部署的服务器节点,帮助网站和应用程序快速、可靠地将内容传递给用户的技术。其主要目的是加速内容的加载速度,降低网络延迟,提高用户体验。

5. 秒杀开关,比如秒杀按钮开关置灰

比如我们要抢某个时间点的高铁票,我们因为害怕错误抢票的最佳时间,往往会提前进到抢票界面。 即使,还没到抢票时间点,但是我们看到抢票按钮可以点击的话,我们都会去多点几下。

如果抢票按钮没置灰的话,会提前有很多请求到服务端,这样服务端压力大很多。所以,为了秒杀开始前,过滤无效的请求到服务端,可以把秒杀按钮置灰

6. MQ 流量削峰、异步处理

回忆一下什么是同步,什么是异步呢?

以方法调用为例,它代表调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在高并发下会造成整体系统性能下降甚至发生雪崩。异步调用恰恰相反,调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。

因此,对于秒杀系统,后端可以借用MQ来做流量削峰。在海量秒杀请求过来时,先放到MQ消息队列中,快速响应用户,告诉用户请求正在处理中,这样就可以释放资源来处理更多的请求。秒杀请求处理完后,通知用户秒杀抢购成功或者失败。

对于秒杀成功的请求,因为还有下单、支付等操作,还可以使用MQ做异步处理。就是把秒杀成功的消息放到MQ,下单等后续操作异步处理。

图片

7. 失败补偿

如果秒杀成功消息已经记录,我们就要保证订单一定要处理成功。如果订单创建失败了,我们要做好监控和告警措施。同时有一定的失败重试机制。

8. 限流

秒杀系统,会存在瞬时大流量过来。我们当然希望,系统能全部请求都正常处理。但是有时候没办法,系统的CPU、网络带宽、内存、线程等资源都是有限的。因此,我们要考虑限流。

如果你的系统每秒扛住的请求是一千,如果一秒钟来了十万请求呢?换个角度就是说,高并发的时候,流量洪峰来了,超过系统的承载能力,怎么办呢?

这时候,我们可以采取限流方案。就是为了保护系统,多余的请求,直接丢弃。

什么是限流:

在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。

可以使用Guava的RateLimiter单机版限流,也可以使用Redis分布式限流,还可以使用阿里开源组件sentinel限流。

4种经典限流算法讲解

9. 服务降级

服务降级是一种在系统负载过高或者出现故障时,为了保证核心功能的可用性而暂时关闭或者限制一些非核心功能的策略。

在秒杀系统中,如果突然有大量用户涌入,会对系统的性能和可用性造成巨大压力。如果不进行服务降级,系统可能会因为超负荷而崩溃,导致用户无法完成购买,这对于商家来说是极为不利的。因此,通过服务降级,可以暂时关闭一些非关键功能,如用户评论、商品推荐等,以保证核心功能——即完成购买的流程,能够正常运行。

数据库死锁如何解决?

数据库死锁是指在多个并发事务中,彼此之间发生相互等待的情况,导致所有事务都无法继续执行的情形。

对线面试官 - MySQL 隔离级别 、锁机制

数据库死锁通常由以下原因导致:

  1. 资源竞争:多个事务试图同时访问相同的资源,如数据库表、行、页或锁,但它们请求资源的顺序不同,导致相互等待。

  2. 未释放资源:事务在使用完资源后未及时释放,导致其他事务无法获得所需的资源。这可能是由于程序错误或异常情况引起的。

  3. 不同事务执行速度不同:某些事务执行速度较慢,持有资源的时间过长,其他事务需要等待释放,可能导致死锁。

  4. 操作数据量过大:事务在持有锁的同时,又请求获取更多的锁,导致互相等待。

解决(避免)死锁的方法包括:

  1. 减少锁的数量:使用更低级别的隔离级别如读提交(Read Committed),而非重复读(Repeatable Read),可以避免特定类型的锁竞争。

  2. 缩短事务持有锁的时间:优化事务处理逻辑,减少事务执行时间,降低发生死锁的可能性。

  3. 确定访问数据的固定顺序:在访问多个资源时,保持一致的访问顺序,以减少死锁的发生。

  4. 降低操作数据的量:减少事务需要操作的数据量,尽可能缩短事务的持有时间,以减少死锁的风险。

这些方法可以有效预防和解决数据库死锁问题,提升系统的并发处理能力和稳定性。

MySQL 只操作同一条记录,也会发生死锁吗?

答案是肯定会的。

因为数据库的锁机制针对的是索引而非记录本身。

在事务中,当我们更新一条记录时,如果使用普通索引作为条件,数据库会先获取普通索引的锁,然后尝试获取主键索引的锁。

若此时有另一个线程已经获得了该记录的主键索引锁,并且同时在其事务中试图获取该记录的普通索引锁,就可能导致死锁的发生。

update my_table set name = 'paidaxing',age = 22 where name = "paidaxingwang";

这个SQL会先对name加锁, 然后再回表对id加锁。

-----

select * from my_table where id = 15 for update;

update my_table set age = 33 where name like "paidaxing%";

-- 以上SQL,会先获取主键的锁,然后再获取name的锁。

为了预防这种死锁情况,可以在应用程序中设定特定的索引获取顺序规则,比如规定只能按照主键索引 -> 普通索引的顺序获取锁。这样可以确保不同线程在获取锁时遵循统一的顺序,从而有效地避免死锁的发生(通过 SQL 保证)。

什么是死锁,如何解决?

死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象。在无外力作用下,它们都无法继续向前推进。这种状态被称为系统处于死锁状态,或者简称系统发生了死锁。这些相互等待的进程被称为死锁进程。

发生死锁的四个必要条件是:

  1. 互斥条件:一个资源每次只能被一个进程或线程使用。

  2. 占有且等待:一个进程因请求资源而阻塞时,继续持有已获得的资源。

  3. 不可抢占:已获得的资源在未使用完之前不可被强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解除死锁可以从以下几个方面入手:

  • 破坏不可抢占条件:设置资源的优先级,允许高优先级的进程可以抢占低优先级进程的资源。

  • 破坏循环等待条件:保证所有进程(线程)请求资源的顺序是一致的,比如按照固定的顺序请求资源,例如 A->B->C,避免形成循环等待。

在数据库中,如果多个事务并发执行,也可能会发生死锁。例如,当事务 1 持有资源 A 的锁,尝试获取资源 B 的锁,同时事务 2 持有资源 B 的锁,尝试获取资源 A 的锁时,就可能导致死锁的发生。发生死锁时,可能会出现如下异常情况:

Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL]

Deadlock found when trying to get lock;

一般来说,对于数据库的死锁问题,主要是要避免并发修改的冲突。另外一种方法是保证操作的顺序,例如多个事务都先操作资源 A,再操作资源 B,这样可以有效地避免死锁的发生。

如何排查死锁问题?您在生产环境中是否遇到过?逐步的排查方法是什么?感兴趣的小伙伴可以点赞收藏,下期出。

6大分库分表中间件详解

分库分表

分库分表:是数据库水平扩展的一种常见策略,用于处理大规模数据、和高并发请求。

图片

分库分表,这里分为两个方面,包含:分库、和分表两种策略。

1、分库

分库:就是将数据水平分散到多个数据库实例中,每个数据库实例可以部署在不同的服务器上,从而减轻单个数据库的压力。

还是举一个例子,比如:数据按照某个字段的值范围,分布到不同的库中。

例如:用户ID为1-10000的数据存储在库A,10001-20000的数据存储在库B,这就是分库。

2、分表

分表:就是在单个数据库实例内,将数据水平分散到多个表中,从而减轻单表的压力。

还是举一个例子,比如:根据某个字段的值范围分表。

例如:用户ID为1-10000的数据存储在表user_1,10001-20000的数据存储在表user_2,这就是分表。

总之,分库分表是数据库扩展中常用的策略,目的是提高系统的并发处理能力、和数据存储能力。

分库分表中间件

在了解完分库分表后,我们一起来看看目前市场主流的分库分表中间件有哪些呢?let's go!

1.ShardingSphere

ShardingSphere 是一个开源的分布式数据库中间件解决方案,支持:分库分表、读写分离、分布式事务、数据加密......等多种功能。

ShardingSphere 通过统一的接口、和高性能的实现,为开发者提供了透明的分库分表支持,适用于Java等应用。

图片

主要包括三个核心组件:

  • Sharding-JDBC:适用于 Java 应用程序,提供 JDBC 层的分库分表支持;

  • Sharding-Proxy:基于 MySQL 协议的代理层,支持多语言应用,客户端无需修改代码,即可实现分库分表;

  • Sharding-Sidecar :适用于 Kubernetes 环境下的微服务架构,提供与:Sharding-JDBC 类似的功能。

ShardingSphere 适用于各种需要分布式数据库管理的场景,比如:电商、社交网络.....等需要处理大规模数据,和高并发请求的场景。

2.TDDL

TDDL,全称是Taobao Distributed Data Layer,就是:淘宝分布式数据层。

TDDL是阿里巴巴集团内部开发的一款分布式数据库中间件,设计目标是:解决在电商业务中遇到的大规模数据、和高并发请求问题。

3.DRDS

阿里的TDDL,后续升级为:DRDS,全称是“Distributed Relational Database Service”,是一款:分布式关系型数据库服务。

图片

DRDS支持:分库分表、读写分离、和分布式事务...等技术。

DRDS广泛应用于数据量大的场景,比如:电商、金融、游戏...等领域。

4.MyCAT

MyCAT 是基于阿里巴巴的 Cobar 项目开发的,是一个增强版的数据库中间件,支持多种数据库(如:MySQL、PostgreSQL...)等。

图片

MyCAT 支持将数据分散到多个数据库实例、和多个表中,通过灵活的分片规则实现水平扩展。

比如:

  • 范围分片:根据某个字段的值范围进行分片;

  • 哈希分片:对某个字段进行哈希计算,然后按哈希值取模分片;

  • 日期分片:根据日期字段进行分片,例如:按月、或按年分片;

5.Atlas

Atlas 是由360 公司开发的一款开源 MySQL 数据库中间件,可以实现 MySQL 数据库的水平扩展、和高可用性。

Atlas 支持分库分表,以及还支持读写分离,将写请求发送到主库,读请求发送到从库,从而提升系统的读写性能和可用性。

6.Vitess

Vitess 是一个开源的分布式数据库中间件系统,最初由 YouTube 开发,用于解决其在大规模 MySQL 集群中的扩展性问题。

Vitess 通过分库分表技术,将数据分散存储到多个 MySQL 实例中,从而提升数据库的存储能力、和并发处理能力。

SpringBoot 过滤器 vs. 拦截器

在 SpringBoot 项目中,过滤器拦截器是两个常用的组件,用于处理请求和响应。虽然它们的功能有些相似,但在实际应用中有不同的使用场景和实现方式。本文将详细讲述过滤器拦截器的区别,并提供实际项目中如何选择的建议。

过滤器 (Filter)

定义

过滤器是一种 Servlet 规范,主要用于在请求到达 Servlet 之前或响应离开 Servlet 之后对请求和响应进行预处理或后处理。

特点

  • 全局性:过滤器可以应用于整个应用的所有请求。

  • 可修改请求和响应:过滤器可以对请求和响应进行修改。

  • 链式调用:多个过滤器可以形成过滤器链,按顺序执行。

使用场景

  • 认证和授权:在请求进入控制器之前检查用户是否已登录或是否有访问权限。

  • 日志记录:记录请求和响应信息。

  • 压缩响应:对响应进行 GZIP 压缩。

实现步骤

创建过滤器类

实现 javax.servlet.Filter 接口。

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化代码,可选实现
        System.out.println("Filter initialized");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 过滤器逻辑在请求处理前执行
        System.out.println("Request received at filter");

        // 继续调用过滤器链,传递请求和响应
        chain.doFilter(request, response);

        // 过滤器逻辑在响应处理后执行
        System.out.println("Response leaving filter");
    }

    @Override
    public void destroy() {
        // 销毁代码,可选实现
        System.out.println("Filter destroyed");
    }
}

注册过滤器(可选,如果不使用 @WebFilter 注解)

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MyFilter> loggingFilter(){
        FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
        
        // 注册自定义过滤器实例
        registrationBean.setFilter(new MyFilter());
        
        // 指定过滤器应用的 URL 模式
        registrationBean.addUrlPatterns("/*");
        
        // 设置过滤器优先级,值越低优先级越高
        registrationBean.setOrder(1);
        
        return registrationBean;
    }
}

拦截器 (Interceptor)

定义

拦截器是 Spring MVC 框架提供的一种机制,用于在请求到达控制器之前和响应离开控制器之后对请求进行拦截和处理。

特点

  • 基于 Spring MVC:拦截器是 Spring MVC 的一部分,只能用于处理控制器请求。

  • 细粒度控制:可以根据 URL 路径进行精细化控制。

  • 不直接修改请求和响应:通常用于处理逻辑而不是修改请求或响应本身。

使用场景

  • 日志记录:记录请求处理时间。

  • 请求处理前后逻辑:在请求处理前后执行一些逻辑,比如安全检查、数据绑定等。

  • 视图处理:在视图渲染前进行处理。

实现步骤

创建拦截器类

实现 HandlerInterceptor 接口。

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 请求处理前逻辑
        System.out.println("Request intercepted at preHandle");
        // 返回 true 继续请求,返回 false 中断请求
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 请求处理后逻辑
        System.out.println("Request intercepted at postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求处理完成后的逻辑
        System.out.println("Request intercepted at afterCompletion");
    }
}

注册拦截器

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义拦截器实例,并指定拦截的路径模式
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**")
            .excludePathPatterns("/login", "/register"); // 可以排除某些路径不被拦截
    }
}

过滤器与拦截器的比较

特性 过滤器 (Filter) 拦截器 (Interceptor)
所属规范 Servlet 规范 Spring MVC 框架
作用范围 整个应用程序的所有请求 仅限于 Spring MVC 的控制器请求
修改请求响应 可以直接修改请求和响应 通常不直接修改请求和响应
配置方式 通过 @WebFilter 注解或 FilterRegistrationBean 通过实现 HandlerInterceptor 并注册
典型应用场景 认证、日志、压缩响应 日志、权限校验、视图处理

实际项目中的选择

在实际项目中,选择过滤器和拦截器应考虑以下几点:

  • 功能需求:如果需要处理与 Servlet 容器相关的操作(如设置响应头),则使用过滤器。如果需要处理 Spring MVC 特有的操作(如访问 Spring 上下文中的 bean ),则使用拦截器。

  • 执行时机:过滤器在请求处理的早期阶段就介入,而拦截器则在 Spring MVC 的 DispatcherServlet 处理请求之后。根据需要在不同阶段执行操作来选择合适的组件。

  • 性能影响:过滤器在每次请求时都会执行,即使请求没有映射到任何 Controller。拦截器只在请求映射到 Controller 时执行。因此,对于性能敏感的应用,应谨慎选择过滤器的使用。

 #{} 和 ${} 的区别

1)**#{}**:使用 #{} 时,MyBatis 为 SQL 语句的参数生成一个预编译语句,并通过参数占位符(?)来代替实际的参数值。这种方式是安全的,因为它防止了 SQL 注入攻击,参数值在发送到数据库之前不会被解析为 SQL 的一部分。

如果你有一个查询用户的操作,你可以这样写:

SELECT * FROM users WHERE id = #{userId}

2)**${}**:相反,使用 ${} 时,参数值被直接替换到 SQL 语句中。这种方式虽然灵活,可以动态替换表名、列名等 SQL 语句的组成部分,但却可能导致 SQL 注入问题,因为参数值如果包含恶意的 SQL 代码也会被执行。

如果你需要动态地选择表名或列名,可以这样使用:

SELECT * FROM ${tableName}

性能考虑

对于性能我得出以下思考:

1)**#{} 性能优势**:由于使用预编译的 SQL 语句,数据库可以重用相同结构的 SQL 语句,减少编译次数,提高执行效率。

2)**${} 性能问题**:可能导致每次都要编译新的 SQL 语句,因为 SQL 结构可能因为参数的不同而改变。

类型处理

1)**#{} 类型处理**:MyBatis 可以根据参数的 Java 类型自动推断数据库类型,处理日期、枚举等复杂类型的转换。

2)**${} 类型处理**:不进行类型转换,将参数视为纯文本,直接嵌入到 SQL 语句中。

鱼聪明 AI 的回答:

#{}(参数占位符)

  • 预编译#{} 表达式使得参数在 SQL 语句中以占位符 ? 的形式出现。这意味着 SQL 语句会被预编译并多次执行,参数值在运行时绑定。这种方式对于数据库性能优化至关重要,因为预编译可以减少数据库编译SQL的次数,提高执行效率。

  • 防止 SQL 注入:由于参数不是直接拼接到 SQL 语句中,而是作为一个绑定的值传递给预编译的 SQL 语句,因此 #{} 防止了 SQL 注入的风险。用户输入被当作字符串处理,不会被执行为 SQL 代码的一部分。

  • 类型安全:MyBatis 能够根据 Java 类型自动推断出相应的 SQL 类型。例如,如果你传递一个 Java Date 对象,MyBatis 会适当地处理它与 SQL 日期类型之间的转换。

${}(SQL 字符串拼接)

  • 直接替换:使用 ${} 时,参数值会在解析 SQL 语句之前直接替换到 SQL 字符串中。这意味着你可以通过 ${} 插入动态表名、列名或者其他 SQL 片段,如动态排序条件。

  • 高风险${} 由于是直接将参数内容拼接到 SQL 语句中,所以存在 SQL 注入的风险。如果参数值来自不可控的用户输入,那么可能被注入恶意的 SQL 代码。

  • 灵活性:尽管风险较高,${} 在某些场景下是必要的。特别是在 SQL 语句的某些部分无法通过预编译实现,如动态改变表名或某些数据库函数和命令,${} 提供了必要的灵活性。

1)`#{}` 和 `${}` 在 MyBatis 中的应用:
   - `#{}`:在 MyBatis 中,`#{}` 用于表示一个占位符,它会被 MyBatis 替换为 `?`,并使用预处理语句(Prepared Statements)来设置 SQL 参数。这种方式可以防止 SQL 注入攻击,因为参数值会被自动处理为安全的参数绑定。
   - `${}`:`${}` 则用于字符串替换,MyBatis 会直接将 `${}` 包围的内容替换为实际的参数值。这种方式不会使用预处理语句,因此可能会受到 SQL 注入攻击的风险。
2)`#{}` 和 `${}` 的工作原理:
   - `#{}` 的工作原理:当 MyBatis 遇到 `#{}` 时,它会创建一个预处理语句(PreparedStatement),并将 `#{}` 中的参数值作为参数绑定到该语句中。这样可以确保参数值被正确地转义,避免 SQL 注入攻击。
   - `${}` 的工作原理:当 MyBatis 遇到 `${}` 时,它会直接将 `${}` 中的内容替换为实际的参数值,然后将整个 SQL 语句编译执行。这种方式下,参数值不会被预处理,因此如果参数值包含恶意 SQL 代码,可能会导致 SQL 注入。
3)`#{}` 和 `${}` 在实际应用中的选择标准:
   - 安全性:如果参数值是用户输入的,应该使用 `#{}` 来避免 SQL 注入风险。
   - 动态 SQL:如果需要在 SQL 语句中动态插入表名、列名或其他 SQL 代码,可以使用 `${}`。但需要注意,使用 `${}` 时应该确保参数值是可信的,或者通过其他方式对参数值进行安全检查。
   - 性能:`#{}` 通常会比 `${}` 有更好的性能,因为它使用了预处理语句。如果不需要动态 SQL,应该优先使用 `#{}`。
   - 可读性:使用 `${}` 可以使 SQL 语句在某些情况下更易读,因为它可以直接插入文本。但这也可能导致 SQL 语句更难以维护和理解,因此需要权衡利弊。

Spring中的声明式事务管理

一、引言

1. 事务管理的重要性

事务管理是数据库操作中的关键环节,确保数据的一致性和完整性。在复杂的业务逻辑中,事务管理能保证操作的原子性,即要么全部成功,要么全部失败。

2. Spring框架中的事务管理简介

Spring框架提供了强大的事务管理支持。它不仅支持编程式事务管理,也支持声明式事务管理。

Spring的事务管理抽象了底层的事务实现,使得开发者可以不用关心底层的事务处理细节,只需要关注业务逻辑。这大大提高了开发效率,也使得代码更加清晰。

二、Spring框架中的事务管理

1. 什么是声明式事务管理

声明式事务管理是一种将事务管理从业务代码中分离出来的方法,它允许你通过注解或XML配置的方式来管理事务,而不是在代码中显式地开始和结束事务。这种方式使得代码更加简洁,易于理解和维护。

在Spring框架中,你可以通过@Transactional注解或在Spring的XML配置文件中配置事务管理。

2. 声明式事务管理与编程式事务管理的比较

  • 编程式事务管理:

    • 通过在代码中显式地调用事务API来控制事务

    • 优点:提供了更高的灵活性,可以在运行时根据需要动态地管理事务。例如,可以根据特定的条件决定是否开始一个新的事务,或者在一个已经存在的事务中执行操作。

    • 缺点:代码的复杂性会增加,因为需要在业务逻辑代码中混入事务管理的代码。这可能会导致代码难以阅读和维护。

  • 声明式事务管理:

    • 通过注解或XML配置的方式来管理事务

    • 优点:可以将事务管理代码和业务逻辑代码分离,使得代码更加清晰,易于阅读和维护。此外,由于事务管理是通过注解或XML配置的方式进行的,因此可以在不修改源代码的情况下改变事务管理的策略。

    • 缺点:灵活性较低,不能在运行时动态地管理事务。例如,不能根据特定的条件决定是否开始一个新的事务,或者在一个已经存在的事务中执行操作。

3. 代码示例

  • 编程式事务

try {
    // 业务逻辑代码
    // ...
    txManager.commit(status);
} catch (Exception e) {
    txManager.rollback(status);
    throw e;
}
  • 声明式事务

import org.springframework.transaction.annotation.Transactional;

@Transactional
public class SomeServiceClass {

    public void someBusinessMethod() {
        // 业务逻辑代码
        // ...
    }
}

三、Spring 声明式事务管理的工作原理

1. Spring AOP(面向切面编程)与事务管理

Spring框架利用AOP(面向切面编程)技术来实现声明式事务管理。

AOP允许开发者定义横切关注点(cross-cutting concerns),这些关注点独立于应用程序的主要业务逻辑。在声明式事务管理中,事务管理就是一个典型的横切关注点。

当使用@Transactional注解时,Spring AOP会在运行时为目标方法创建一个代理对象。这个代理对象负责在方法执行前后执行事务的相关操作。如果方法执行成功,则提交事务;如果方法抛出异常,则回滚事务。这个过程对开发者来说是透明的,他们不需要编写任何事务控制的代码。

2. 事务管理器(Transaction Manager)

事务管理器是Spring声明式事务管理的核心组件,它负责协调和管理事务。Spring提供了多种类型的事务管理器,以支持不同的数据源和事务需求。例如,DataSourceTransactionManager用于JDBC事务管理,而HibernateTransactionManager用于Hibernate事务管理。

事务管理器通过配置与特定的数据源关联,并负责执行以下任务:

  • 开启一个新的事务。

  • 提交当前事务。

  • 回滚当前事务。

  • 获取当前事务的状态。

在Spring配置中,开发者需要定义一个事务管理器Bean,并配置其数据源和其他相关属性。然后,Spring AOP使用这个事务管理器来管理通过@Transactional注解标记的方法的事务。

3. 事务属性(Transaction Attributes)

事务属性定义了事务的行为和范围。在Spring中,事务属性可以通过@Transactional注解的属性来设置。以下是一些常用的事务属性:

  • propagation:定义事务的传播行为,例如是否需要新事务、是否加入到现有事务中等。

  • isolation:设置事务的隔离级别,如读未提交、读已提交、可重复读或串行化。

  • timeout:指定事务的超时时间,以防止长时间运行的事务占用资源。

  • readOnly:标记事务是否为只读,这对于提高性能和避免不必要的写操作很有用。

通过合理设置事务属性,开发者可以根据业务需求定制事务的行为,确保数据的一致性和系统的可靠性。

四、Spring声明式事务管理的实现

1. 使用@Transactional注解实现声明式事务管理

在Spring中,我们可以通过在方法或类上使用@Transactional注解来实现声明式事务管理。当注解在类上时,该类的所有公共方法都会被视为需要进行事务管理的方法。当注解在方法上时,只有该方法需要进行事务管理。此外,@Transactional注解还可以设置事务的属性,如传播行为、隔离级别等。

例如:

@Service
@Transactional
public class UserServiceImpl implements UserService {
// ...
}

2. 使用XML配置实现声明式事务管理

除了使用注解,我们还可以通过XML配置来实现声明式事务管理。在XML配置文件中,我们可以定义一个或多个事务管理器,并通过aop:config元素来定义哪些方法需要进行事务管理,以及它们的事务属性。

例如:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<aop:config>
    <aop:pointcut id="transactionPointcut" expression="execution(* com.example.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

在这个例子中,所有在com.example.service包下的方法都会进行事务管理,事务的传播行为为REQUIRED

五、Spring 声明式事务管理的优点和局限性

Spring 声明式事务管理提供了一种简洁、易用的方式来管理事务。它允许开发人员通过注解或 XML 配置来声明事务的边界,而无需编写复杂的事务控制代码。然而,正如任何技术一样,声明式事务管理也有一些优点和局限性。

1. 优点:

  • 简化代码:声明式事务管理减少了手动编写事务控制代码的需要,使代码更加简洁、清晰。这有助于提高开发效率和代码的可读性。

  • 解耦:通过将事务管理与业务逻辑分离,可以更容易地修改或替换事务管理策略,而不影响业务逻辑的实现。这有助于实现更好的模块化和可维护性。

  • 一致性:声明式事务管理确保在方法执行过程中始终遵循一致的事务处理逻辑,减少了因手动管理事务而导致的错误和不一致的可能性。

  • 支持多种事务管理器:Spring 提供了对各种事务管理器的支持,如 JDBC、Hibernate、JPA 等。这使得开发人员可以根据项目需求选择合适的事务管理器。

  • 灵活性:可以在不修改源代码的情况下改变事务管理的策略,例如,可以通过修改配置文件来改变事务的传播行为。

2. 局限性:

  • 性能开销:虽然声明式事务管理简化了代码,但它也引入了额外的性能开销。每次事务的开启、提交和关闭都需要一定的时间,特别是在高并发场景下,这些开销可能会对性能产生影响。

  • 异常处理:声明式事务管理默认只在运行时异常(RuntimeException)发生时回滚事务。对于检查型异常(Checked Exception),需要显式地指定回滚规则。这可能导致在某些情况下无法正确回滚事务。

  • 事务传播行为:声明式事务管理的事务传播行为有时可能不符合预期。例如,当两个相互关联的方法分别属于不同的服务类时,可能需要仔细考虑事务的传播行为以避免潜在的问题。

  • 不支持跨远程调用的事务管理:声明式事务管理主要适用于单个应用服务器内的场景。在分布式系统中,涉及跨远程调用的事务管理可能会遇到挑战,因为它需要协调不同服务器上的事务状态。

六、声明式事务的失效的常见原因

1.  @Transactional 应用在非public方法上

Spring的声明式事务管理是通过AOP(面向切面编程)来实现的,具体来说,就是通过动态代理的方式。当一个类被Spring代理后,调用该类的方法实际上是调用了代理对象的方法。在这个过程中,Spring会在方法执行前后添加事务管理的代码。

然而,Spring只能代理public方法。这是因为在Java中,protectedprivate和默认(package-private)访问级别的方法只能在同一个类或同一个包中被访问,而Spring创建的代理对象是在一个新的类中,因此无法访问这些非public方法。所以,当@Transactional注解应用在非public方法上时,Spring无法创建正确的代理对象,导致事务管理的代码无法被正确添加,从而使得事务失效。

2. 事务方法在同一个类中被直接调用

Spring的AOP是基于代理的,只有通过代理对象调用的方法才会触发事务管理。如果在同一个类中,一个方法直接调用另一个方法,那么被调用的方法的@Transactional注解将不会起作用。

3. 异常类型不匹配

Spring的声明式事务默认只在方法抛出unchecked异常(即RuntimeException及其子类)或Error时进行回滚。如果方法抛出的是checked异常,那么事务不会回滚。你可以通过在@Transactional注解中配置rollbackFor属性,使其在checked异常也触发事务回滚。

4. 事务传播行为不正确

在Spring中,你可以通过@Transactional注解的propagation属性来配置事务的传播行为。如果这个配置不正确,那么可能会导致事务不起作用。

5. 数据库不支持事务

并非所有的数据库都支持事务,如果你使用的数据库不支持事务,那么Spring的声明式事务管理自然也就不会起作用。

6. 事务管理器配置错误

如果事务管理器没有正确配置,那么Spring的声明式事务管理也将不会起作用。你需要确保在Spring的配置文件中正确配置了事务管理器,并且这个事务管理器能够正确管理你的数据库连接。

如何保证线程安全?

随着硬件技术的快速发展(比如多核处理器,超线程技术),我们通常会在代码中使用多线程(比如线程池)来提高性能,但是,多线程又会带来线程安全问题。

1.什么是线程安全?

首先,我们来看看维基百科对线程安全是如何描述的,如下图:

图片

总结一下:线程安全(Thread Safety)是指多个线程访问共享资源时,不会破坏资源的完整性。如下图:

图片

请注意,导致线程安全问题一定要同时具备以下 3个条件,缺一不可:

  • 1. 多线程环境:如果是单线程,程序肯定会串行顺序执行,不可能出现线程安全问题。

  • 2. 操作共享资源:所谓共享资源是指多个线程或进程可以同时访问和使用的资源。如果每个线程都是操作自己的局部变量,尽管满足条件1,但也不会出现线程安全问题。

  • 3. 至少存在一个写操作:如果是多线程读取共享资源,尽管满足了前 2个条件,但是读操作天然是幂等的,因此也不会出现线程安全的问题,所以线程中至少存在一个写操作。

上面从表象上说明线程安全需要具备的 3个条件,在 Java中,线程安全性通常涉及以下 3个指标:

  • 原子性(Atomicity):操作要么全部完成,要么全部不完成。

  • 可见性(Visibility):一个线程对共享变量的修改对其他线程是立即可见的。

  • 有序性(Ordering):程序的执行顺序符合预期,不会因为编译器优化或CPU重排序而改变。

2. 产生线程安全的根因

在 Java中,造成线程安全问题的根因是硬件结构,为了消除 CPU和主内存之间的硬件速度差,通常会在两者之间设置多级缓存(L1 ~ L3),如下图:

图片

Java为了适配这种多级缓存的硬件构造,设计了一套与之对应的内存模型(JMM,Java memory model,包括主内存和工作内存,如下图:

图片

  • 主内存:所有的变量都存储在主内存中。

  • 工作内存:每个线程都有自己的工作内存,会将主内存的共享变量复制到自己的工作内存中,然后做后续业务操作,最终再将工作内存中的变量刷新到主内存。

线程对变量的所有操作(读取、写入)都必须在工作内存中进行,而不能直接读写主内存中的变量。线程之间无法直接访问对方的工作内存,变量的传递需要通过主内存来完成。

3. 原子性

在数据库事务ACID中也有原子性(Atomicity)的概念,它是指一个操作是不可分割的,即要么全部执行,要么全部不执行。Java线程安全中的原子性与数据库事务中的原子性本质是一样的,只是它们应用的上下文和具体实现有所不同。

Java提供了多种方式来保证原子性,比如 同步块、锁或者原子类。

为了更好的说明原子性,我们这里以一个反例来展示不具备原子性,如下代码:

public class AtomicityTest {
    private int i = 0;
    public void increment() {
        i++;
    }
}

在上述代码中,i++这种写法在我们的日常开发经常使用,但它不是一个原子操作,实际上i++分为三步:

  • 读取i的值

  • 将i的值加 1

  • 将结果写回给i

如果多个线程同时执行increment()方法,可能会导致i的值不正确,比如有 3个线程A,B,C:

  • 线程A读取i的值,并且将i的值加 1,但是还未将结果写回给i;

  • 此时,线程B读取i的值仍然是0,并且将i的值加 1;

  • 线程A 将结果写回给i,将i设置为 1;

  • 线程B 将结果写回给i,将i设置为 1;

  • 线程C 读取i的值为1,并且将i的值加 1,并且将结果写回给i,将i设置为 2;

3个线程都对i进行i++操作,预期i的最终值是 3,但因为i++无法保证原子性,因此,i最终的值未达到预期的值。

4. 可见性

可见性是指一个线程对共享变量的修改,其他线程能立刻看到。在Java中,volatile关键字可以保证变量的可见性。

为了更好的说明可见性,我们这里以一个示例进行分析,如下代码:

public class VisibilityTest {
    private boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // do something
        }
    }
}

在上述代码中,变量running是一个全局变量,如果没有使用volatile关键字,running 变量的修改可能不会被其他线程立即看到。

5. 有序性

有序性是指程序代码的执行顺序。在单线程环境中,代码的执行顺序通常是按照代码的书写顺序执行的。然而,在多线程环境中,编译器、JVM和CPU可能会为了优化性能进行指令重排序(Instruction Reordering),这可能会导致代码的执行顺序与预期不一致。

Java内存模型(Java Memory Model, JMM)允许编译器和处理器进行指令重排序,但会保证单线程内的执行结果和多线程内的同步结果是正确的。这里以一个反例来展示不具备有序性,如下代码:

public class ReorderingExample {
private int x = 0;
private boolean flag = false;

    public void writer() {
        x = 42;
        flag = true;
    }

    public void read() {
        if (flag) {
            System.out.println(x); // 可能输出0
        }
    }
}

在上述代码中,read()方法可能会看到flag=true,但x仍然为 0,因为编译器或CPU可能对指令进行重排序。

6. 如何保证线程安全

在 Java中,通常可以通过以下几个方式来保证线程安全。

synchronized关键字

synchronized是Java的一个原语关键字,它可以保证方法或代码块在同一时刻只能被一个线程执行,从而确保原子性和可见性。

下面的代码是synchronized关键字的简单使用:

public class SynchronizedTest {
private int i = 0;
    public synchronized void increment() {
        i++;
    }
    public synchronized int getCount() {
        return i;
    }
}

Lock 接口

Lock接口提供了比synchronized更灵活的锁机制,常用的实现类有 ReentrantLock 可重入锁。

下面的代码是Lock关键字的简单使用:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

原子类

Java提供了一些原子类,如 AtomicInteger、AtomicLong 和 AtomicReference,它们通过CAS(Compare-And-Swap)操作实现了非阻塞的线程安全。

下面的代码是AtomicInteger原子类的简单使用:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicTest {
private AtomicInteger atomic = new AtomicInteger();

    public void increment() {
        atomic.incrementAndGet();
    }

    public int getCount() {
        return atomic.get();
    }
}

ThreadLocal 类

ThreadLocal类提供了线程局部变量,每个线程都有自己独立的变量副本,从而避免了共享数据的竞争。

下面的代码是ThreadLocal类的简单使用:

public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    public int getValue() {
        return threadLocal.get();
    }

    public void setValue(int value) {
        threadLocal.set(value);
    }
}

分布式锁

Redis 分布式锁 或者 Zookeeper分布式锁是分布式环境下保证线程安全的常用方法。

7. 总结

线程安全是 Java多线程编程中很重要的一部分,本文讲解了什么是线程安全以及产生线程安全问题的根因,并且通过原子性,有序性,可见性对线程安全进行了分析。

  • 硬件的多级缓存和Java与之对应的内存模型是导致线程安全的根因;

  • volatile可以保证变量的可见性,但不能保证原子性,因此无法保证线程安全;

  • synchronized,虚拟机锁,原子类,分布式锁可以保证线程的安全性;

`ThreadLocal` 是 Java 中一个用于实现线程局部变量的类。它的实现原理主要依赖于每个线程内部的一个映射表,这个映射表的键是 `ThreadLocal` 对象,值是要存储的线程局部变量。这样,每个线程都可以独立地存取自己的局部变量,而不会影响到其他线程。
以下是 `ThreadLocal` 实现原理的详细解释:
1. **线程局部变量存储**:`ThreadLocal` 类中有一个内部类 `ThreadLocalMap`,这个类本质上是一个哈希表,用于存储线程局部变量。每个线程都有一个自己的 `ThreadLocalMap` 实例。
2. **键值对存储**:在 `ThreadLocalMap` 中,`ThreadLocal` 对象作为键(key),要存储的线程局部变量作为值(value)。这样,每个 `ThreadLocal` 对象都对应一个线程局部变量。
3. **线程隔离**:由于每个线程都有自己的 `ThreadLocalMap`,所以不同线程的局部变量是相互隔离的。这意味着一个线程的局部变量不会被其他线程访问或修改。
4. **访问和设置变量**:当线程需要访问或设置局部变量时,它会通过 `ThreadLocal` 对象进行操作。`ThreadLocal` 对象会查找当前线程的 `ThreadLocalMap`,并从中获取或设置对应的局部变量。
5. **弱引用键**:`ThreadLocalMap` 中的键(`ThreadLocal` 对象)被设计为弱引用。这意味着,如果 `ThreadLocal` 对象只被 `ThreadLocalMap` 中的弱引用所引用,那么它可以在垃圾回收时被回收。这有助于防止内存泄漏,因为如果线程长时间运行,而 `ThreadLocal` 对象不再被使用,它占用的内存可以被释放。
6. **内存泄漏问题**:尽管 `ThreadLocal` 使用了弱引用来避免内存泄漏,但如果线程局部变量值本身是强引用,那么即使 `ThreadLocal` 对象被回收,值仍然不会被回收,可能导致内存泄漏。因此,在使用 `ThreadLocal` 时,应该确保在不需要时手动清除线程局部变量,以避免内存泄漏。
总结来说,`ThreadLocal` 的实现原理是通过为每个线程提供一个独立的变量副本,从而实现线程间的数据隔离。这种设计使得 `ThreadLocal` 非常适合用于管理线程私有的数据,例如数据库连接、事务等。 

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 17:20:02       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 17:20:02       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 17:20:02       45 阅读
  4. Python语言-面向对象

    2024-07-20 17:20:02       55 阅读

热门阅读

  1. linux shell(上)

    2024-07-20 17:20:02       20 阅读
  2. RK3588 编译opencv&opencv_contrib记录

    2024-07-20 17:20:02       25 阅读
  3. 二叉树---路径总和

    2024-07-20 17:20:02       18 阅读
  4. windows 安装 kubectl 并连接到 k8s 集群【图文教程】

    2024-07-20 17:20:02       21 阅读
  5. computeIfAbsent 和 putIfAbsent

    2024-07-20 17:20:02       20 阅读
  6. 微软Edge浏览器全解析教程

    2024-07-20 17:20:02       18 阅读
  7. 如何使用unittest框架来编写和运行单元测试

    2024-07-20 17:20:02       20 阅读
  8. 数学建模熵权法

    2024-07-20 17:20:02       21 阅读
  9. RabbitMQ线程和连接模型详解

    2024-07-20 17:20:02       23 阅读
  10. 探索现代Web开发:WebKit的剪贴板API革新

    2024-07-20 17:20:02       26 阅读