乐闻世界logo
搜索文章和话题

服务端面试题手册

Spring Boot 中如何实现异步编程?

Spring Boot 异步编程详解为什么需要异步编程提高吞吐量:不阻塞主线程,处理更多请求改善响应时间:耗时操作后台执行,快速响应用户资源利用:充分利用 CPU 多核特性解耦操作:发送消息、记录日志等非核心操作异步化实现方式一:@Async 注解(最常用)1. 开启异步支持@Configuration@EnableAsyncpublic class AsyncConfig { /** * 自定义线程池 */ @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数 executor.setCorePoolSize(5); // 最大线程数 executor.setMaxPoolSize(20); // 队列容量 executor.setQueueCapacity(100); // 线程名称前缀 executor.setThreadNamePrefix("async-task-"); // 拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务完成后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; } /** * 邮件发送专用线程池 */ @Bean("mailExecutor") public Executor mailExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(50); executor.setThreadNamePrefix("mail-task-"); executor.initialize(); return executor; }}2. 使用 @Async 注解@Service@Slf4jpublic class AsyncService { /** * 默认线程池执行 */ @Async public void sendNotification(String userId, String message) { log.info("发送通知给用户: {}, 线程: {}", userId, Thread.currentThread().getName()); // 模拟耗时操作 try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("通知发送完成: {}", userId); } /** * 指定线程池 */ @Async("mailExecutor") public void sendEmail(String to, String subject, String content) { log.info("发送邮件到: {}, 线程: {}", to, Thread.currentThread().getName()); // 邮件发送逻辑 } /** * 带返回值的异步方法 */ @Async public CompletableFuture<String> processDataAsync(String data) { log.info("异步处理数据: {}, 线程: {}", data, Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("处理结果: " + data); } /** * 批量异步处理 */ @Async public CompletableFuture<Integer> calculateAsync(int number) { int result = number * number; return CompletableFuture.completedFuture(result); }}3. 调用异步方法@RestController@RequestMapping("/async")@RequiredArgsConstructorpublic class AsyncController { private final AsyncService asyncService; @GetMapping("/notify") public Result<Void> sendNotification(@RequestParam String userId) { // 立即返回,不等待异步操作完成 asyncService.sendNotification(userId, "您有一条新消息"); return Result.success("通知发送中"); } @GetMapping("/process") public Result<String> processData(@RequestParam String data) throws Exception { // 调用异步方法并等待结果 CompletableFuture<String> future = asyncService.processDataAsync(data); // 设置超时 String result = future.get(5, TimeUnit.SECONDS); return Result.success(result); } @GetMapping("/batch") public Result<List<Integer>> batchProcess() throws Exception { // 批量异步处理 List<CompletableFuture<Integer>> futures = IntStream.range(1, 11) .mapToObj(asyncService::calculateAsync) .collect(Collectors.toList()); // 等待所有任务完成 CompletableFuture<Void> allFutures = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); // 获取所有结果 allFutures.join(); List<Integer> results = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); return Result.success(results); }}实现方式二:CompletableFuture(Java 8+)@Service@Slf4jpublic class CompletableFutureService { private final ExecutorService executor = Executors.newFixedThreadPool(10); /** * 异步执行并组合结果 */ public CompletableFuture<String> combineAsyncTasks() { CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> { log.info("执行任务1"); sleep(1000); return "结果1"; }, executor); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> { log.info("执行任务2"); sleep(1500); return "结果2"; }, executor); // 组合两个任务的结果 return task1.thenCombine(task2, (result1, result2) -> { return result1 + " + " + result2; }); } /** * 异步任务链 */ public CompletableFuture<String> taskChain(String input) { return CompletableFuture.supplyAsync(() -> { log.info("步骤1: 处理输入"); return input.toUpperCase(); }, executor) .thenApplyAsync(result -> { log.info("步骤2: 添加前缀"); return "PREFIX_" + result; }, executor) .thenApplyAsync(result -> { log.info("步骤3: 添加后缀"); return result + "_SUFFIX"; }, executor) .exceptionally(ex -> { log.error("任务链异常: {}", ex.getMessage()); return "DEFAULT_VALUE"; }); } /** * 并行处理多个任务 */ public List<String> parallelProcess(List<String> inputs) { List<CompletableFuture<String>> futures = inputs.stream() .map(input -> CompletableFuture.supplyAsync(() -> processSingle(input), executor)) .collect(Collectors.toList()); // 等待所有完成并收集结果 return futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); } private String processSingle(String input) { sleep(500); return "Processed: " + input; } /** * 带超时的异步操作 */ public String asyncWithTimeout() { try { return CompletableFuture.supplyAsync(() -> { sleep(5000); // 模拟耗时操作 return "结果"; }, executor) .orTimeout(3, TimeUnit.SECONDS) // Java 9+ .exceptionally(ex -> "超时默认值") .get(); } catch (Exception e) { return "异常: " + e.getMessage(); } } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}实现方式三:Spring WebFlux(响应式编程)@Configuration@EnableWebFluxpublic class WebFluxConfig {}@RestController@RequestMapping("/reactive")public class ReactiveController { @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamData() { return Flux.interval(Duration.ofSeconds(1)) .map(seq -> "Data " + seq + " at " + LocalTime.now()) .take(10); } @GetMapping("/mono/{id}") public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) { return Mono.fromCallable(() -> findUserById(id)) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); } @PostMapping("/users") public Mono<User> createUser(@RequestBody Mono<User> userMono) { return userMono.flatMap(this::saveUser); } @GetMapping("/users") public Flux<User> getAllUsers() { return Flux.fromIterable(findAllUsers()); } // 模拟数据库操作 private User findUserById(Long id) { // 模拟异步数据库查询 return new User(id, "User" + id); } private Mono<User> saveUser(User user) { return Mono.just(user) .map(u -> { u.setId(System.currentTimeMillis()); return u; }); } private List<User> findAllUsers() { return Arrays.asList( new User(1L, "User1"), new User(2L, "User2"), new User(3L, "User3") ); }}@Data@AllArgsConstructorclass User { private Long id; private String name;}实现方式四:DeferredResult(Servlet 异步)@RestController@RequestMapping("/deferred")@RequiredArgsConstructorpublic class DeferredResultController { private final AsyncService asyncService; @GetMapping("/task") public DeferredResult<String> asyncTask() { DeferredResult<String> deferredResult = new DeferredResult<>(10000L, "超时"); // 异步处理 new Thread(() -> { try { Thread.sleep(3000); deferredResult.setResult("任务完成"); } catch (InterruptedException e) { deferredResult.setErrorResult("任务中断"); } }).start(); // 超时回调 deferredResult.onTimeout(() -> { System.out.println("任务超时"); }); // 完成回调 deferredResult.onCompletion(() -> { System.out.println("任务完成回调"); }); return deferredResult; } @GetMapping("/callable") public Callable<String> callableTask() { return () -> { Thread.sleep(3000); return "Callable 任务完成"; }; }}异步事务处理@Service@Slf4jpublic class AsyncTransactionalService { /** * 异步方法中的事务 * 注意:@Async 和 @Transactional 可以一起使用 */ @Async @Transactional public CompletableFuture<Void> asyncTransactionalOperation() { // 事务操作 log.info("异步事务操作,线程: {}", Thread.currentThread().getName()); return CompletableFuture.completedFuture(null); } /** * 在异步方法中调用事务方法 */ @Async public CompletableFuture<Void> callTransactionalMethod() { // 调用另一个类的 @Transactional 方法 transactionalService.doTransaction(); return CompletableFuture.completedFuture(null); }}异步异常处理@Configuration@Slf4jpublic class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { log.error("异步方法异常 - 方法: {}, 参数: {}", method.getName(), params, ex); // 可以在这里发送告警、记录日志等 sendAlert(method.getName(), ex.getMessage()); } private void sendAlert(String methodName, String errorMessage) { // 发送告警通知 log.warn("发送告警: 方法 {} 执行失败,错误: {}", methodName, errorMessage); }}// 在配置类中注册@Configuration@EnableAsyncpublic class AsyncConfig implements AsyncConfigurer { @Override @Bean(name = "taskExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncExceptionHandler(); }}线程池监控@Component@Slf4jpublic class ThreadPoolMonitor { @Autowired @Qualifier("taskExecutor") private ThreadPoolTaskExecutor taskExecutor; @Scheduled(fixedRate = 60000) // 每分钟执行 public void monitor() { ThreadPoolExecutor executor = taskExecutor.getThreadPoolExecutor(); log.info("线程池状态 - 核心线程数: {}, 活跃线程数: {}, 完成任务数: {}, " + "队列任务数: {}, 总任务数: {}", executor.getCorePoolSize(), executor.getActiveCount(), executor.getCompletedTaskCount(), executor.getQueue().size(), executor.getTaskCount()); }}最佳实践1. 线程池配置建议@Bean("ioIntensiveExecutor")public Executor ioIntensiveExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // IO 密集型:核心线程数可以设置较大 executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2); executor.setMaxPoolSize(100); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("io-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor;}@Bean("cpuIntensiveExecutor")public Executor cpuIntensiveExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // CPU 密集型:核心线程数 = CPU 核心数 + 1 executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1); executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() + 1); executor.setQueueCapacity(100); executor.setThreadNamePrefix("cpu-task-"); executor.initialize(); return executor;}2. 避免异步陷阱@Service@Slf4jpublic class AsyncPitfallService { /** * ❌ 错误:同类中调用 @Async 方法不会异步执行 */ public void wrongAsyncCall() { // 这样调用不会走代理,不会异步执行 this.asyncMethod(); } @Async public void asyncMethod() { // 异步逻辑 } /** * ✅ 正确:通过注入的代理对象调用 */ @Autowired private AsyncPitfallService self; public void correctAsyncCall() { // 通过代理对象调用 self.asyncMethod(); }}3. 异步方法返回值@Servicepublic class AsyncReturnService { /** * ❌ 不推荐:返回 void,无法知道执行结果 */ @Async public void fireAndForget() { // 执行操作 } /** * ✅ 推荐:返回 CompletableFuture,可以获取结果和异常 */ @Async public CompletableFuture<String> asyncWithResult() { try { // 执行操作 return CompletableFuture.completedFuture("success"); } catch (Exception e) { CompletableFuture<String> future = new CompletableFuture<>(); future.completeExceptionally(e); return future; } }}总结| 方式 | 适用场景 | 优点 | 缺点 ||------|---------|------|------|| @Async | 简单异步任务 | 简单易用 | 功能有限 || CompletableFuture | 复杂异步编排 | 功能强大、可组合 | 学习曲线 || WebFlux | 高并发响应式 | 非阻塞、高性能 | 编程模型不同 || DeferredResult | Servlet 异步 | 兼容传统 MVC | 较底层 |选择建议:简单后台任务:@Async复杂异步流程:CompletableFuture高并发 API:WebFlux传统项目升级:DeferredResult
阅读 0·3月6日 21:58

Spring Boot 中如何实现微服务架构的服务注册与发现?

Spring Boot 微服务注册与发现详解为什么需要服务注册与发现在微服务架构中,服务实例动态变化:服务扩容/缩容:实例数量不断变化故障转移:故障实例需要自动剔除动态路由:客户端需要知道可用服务地址负载均衡:在多个实例间分发请求主流注册中心对比| 特性 | Eureka | Nacos | Consul | Zookeeper ||------|--------|-------|--------|-----------|| 开发公司 | Netflix | 阿里巴巴 | HashiCorp | Apache || 一致性协议 | AP | AP/CP | CP | CP || 健康检查 | 客户端心跳 | TCP/HTTP/MySQL | TCP/HTTP/gRPC | 临时节点 || 多数据中心 | 支持 | 支持 | 支持 | 不支持 || Spring Cloud | 原生支持 | 支持 | 支持 | 支持 || 配置中心 | 不支持 | 支持 | 支持 | 不支持 |实现方式一:Eureka1. Eureka Server 配置添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>启动类@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}配置文件server: port: 8761spring: application: name: eureka-servereureka: instance: hostname: localhost client: # 不向注册中心注册自己 register-with-eureka: false # 不需要检索服务 fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ server: # 关闭自我保护机制 enable-self-preservation: false # 清理间隔(毫秒) eviction-interval-timer-in-ms: 50002. Eureka Client 配置添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>配置文件server: port: 8081spring: application: name: user-serviceeureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: # 使用 IP 地址注册 prefer-ip-address: true # 实例 ID instance-id: ${spring.application.name}:${server.port} # 心跳间隔 lease-renewal-interval-in-seconds: 5 # 服务过期时间 lease-expiration-duration-in-seconds: 103. Eureka 高可用集群# eureka-server-1.ymlserver: port: 8761spring: application: name: eureka-servereureka: instance: hostname: eureka1 client: service-url: defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/# eureka-server-2.ymlserver: port: 8762eureka: instance: hostname: eureka2 client: service-url: defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/# eureka-server-3.ymlserver: port: 8763eureka: instance: hostname: eureka3 client: service-url: defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/实现方式二:Nacos(推荐)1. Nacos Server 部署# 下载并启动 Nacoscurl -O https://github.com/alibaba/nacos/releases/download/2.2.3/nacos-server-2.2.3.tar.gztar -xzf nacos-server-2.2.3.tar.gzcd nacos/bin# 单机模式启动sh startup.sh -m standalone# 访问控制台# http://localhost:8848/nacos# 默认账号密码:nacos/nacos2. Nacos Client 配置添加依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>配置文件server: port: 8081spring: application: name: user-service cloud: nacos: discovery: # Nacos 服务器地址 server-addr: localhost:8848 # 命名空间(环境隔离) namespace: dev # 分组 group: DEFAULT_GROUP # 元数据 metadata: version: v1 region: beijing # 权重 weight: 1 # 临时实例(false 为持久实例) ephemeral: true # 心跳间隔 heart-beat-interval: 5000 # 心跳超时 heart-beat-timeout: 150003. Nacos 服务发现@RestController@RequestMapping("/discovery")@RequiredArgsConstructorpublic class DiscoveryController { private final DiscoveryClient discoveryClient; private final LoadBalancerClient loadBalancerClient; /** * 获取所有服务实例 */ @GetMapping("/services") public List<String> getServices() { return discoveryClient.getServices(); } /** * 获取指定服务的实例 */ @GetMapping("/instances/{serviceName}") public List<ServiceInstance> getInstances(@PathVariable String serviceName) { return discoveryClient.getInstances(serviceName); } /** * 使用负载均衡选择实例 */ @GetMapping("/choose/{serviceName}") public ServiceInstance choose(@PathVariable String serviceName) { return loadBalancerClient.choose(serviceName); }}实现方式三:Consul1. Consul Server 部署# 下载 Consul# https://www.consul.io/downloads# 开发模式启动consul agent -dev# 生产模式(集群)consul agent -server -bootstrap-expect=3 -data-dir=/var/consul \ -bind=192.168.1.1 -client=0.0.0.0 -ui -retry-join="provider=aws ..."2. Consul Client 配置添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId></dependency>配置文件server: port: 8081spring: application: name: user-service cloud: consul: host: localhost port: 8500 discovery: # 注册服务 register: true # 服务名称 service-name: ${spring.application.name} # 健康检查路径 health-check-path: /actuator/health # 健康检查间隔 health-check-interval: 10s # 实例 ID instance-id: ${spring.application.name}:${random.value} # 标签 tags: version=1.0,profile=dev # 元数据 metadata: version: v1 region: beijing服务间调用1. 使用 RestTemplate + @LoadBalanced@Configurationpublic class RestTemplateConfig { @Bean @LoadBalanced // 开启负载均衡 public RestTemplate restTemplate() { return new RestTemplate(); }}@Service@RequiredArgsConstructorpublic class UserService { private final RestTemplate restTemplate; public Order getOrderByUserId(Long userId) { // 使用服务名调用,而非具体 IP String url = "http://order-service/orders/user/" + userId; return restTemplate.getForObject(url, Order.class); }}2. 使用 OpenFeign(推荐)添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>启用 Feign@SpringBootApplication@EnableFeignClientspublic class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); }}定义 Feign 客户端@FeignClient( name = "order-service", fallback = OrderClientFallback.class, configuration = FeignConfig.class)public interface OrderClient { @GetMapping("/orders/{id}") Order getOrderById(@PathVariable("id") Long id); @GetMapping("/orders/user/{userId}") List<Order> getOrdersByUserId(@PathVariable("userId") Long userId); @PostMapping("/orders") Order createOrder(@RequestBody Order order);}@Component@Slf4jpublic class OrderClientFallback implements OrderClient { @Override public Order getOrderById(Long id) { log.warn("Order service is down, returning fallback for order: {}", id); Order order = new Order(); order.setId(id); order.setStatus("UNKNOWN"); return order; } @Override public List<Order> getOrdersByUserId(Long userId) { return Collections.emptyList(); } @Override public Order createOrder(Order order) { throw new RuntimeException("Order service is unavailable"); }}Feign 配置@Configurationpublic class FeignConfig { @Bean public Retryer feignRetryer() { // 重试策略:初始间隔 100ms,最大间隔 1s,最多重试 3 次 return new Retryer.Default(100, 1000, 3); } @Bean public ErrorDecoder feignErrorDecoder() { return new CustomErrorDecoder(); } @Bean public RequestInterceptor feignRequestInterceptor() { return requestTemplate -> { // 添加请求头 requestTemplate.header("X-Request-Id", UUID.randomUUID().toString()); // 传递认证信息 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); String token = request.getHeader("Authorization"); if (StringUtils.hasText(token)) { requestTemplate.header("Authorization", token); } } }; }}负载均衡1. Ribbon(已弃用,Spring Cloud 2020+ 使用 Spring Cloud LoadBalancer)# 自定义负载均衡策略user-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 其他策略:RoundRobinRule, WeightedResponseTimeRule, BestAvailableRule2. Spring Cloud LoadBalancer@Configurationpublic class LoadBalancerConfig { @Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer( Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new ReactorServiceInstanceLoadBalancer() { private Random random = new Random(); @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class) .getIfAvailable(); return supplier.get().next().map(instances -> { if (instances.isEmpty()) { return new EmptyResponse(); } // 随机选择 int index = random.nextInt(instances.size()); return new DefaultResponse(instances.get(index)); }); } }; }}服务网关(Gateway)<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId></dependency>server: port: 8080spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true # 自动从注册中心发现服务 lower-case-service-id: true routes: # 用户服务路由 - id: user-service uri: lb://user-service predicates: - Path=/api/users/** filters: - StripPrefix=1 - name: Retry args: retries: 3 statuses: BAD_GATEWAY # 订单服务路由 - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: - StripPrefix=1 - name: CircuitBreaker args: name: orderServiceCircuitBreaker fallbackUri: forward:/fallback/order # 全局过滤器 default-filters: - AddRequestHeader=X-Request-From, Gateway - AddResponseHeader=X-Response-From, Gateway配置中心集成(Nacos Config)<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency># bootstrap.ymlspring: application: name: user-service profiles: active: dev cloud: nacos: config: server-addr: localhost:8848 file-extension: yaml namespace: dev group: DEFAULT_GROUP # 共享配置 shared-configs: - data-id: common.yaml group: DEFAULT_GROUP refresh: true # 扩展配置 extension-configs: - data-id: redis.yaml group: DEFAULT_GROUP refresh: true@RestController@RefreshScope // 配置自动刷新public class ConfigController { @Value("${user.name:default}") private String userName; @Value("${user.age:0}") private Integer userAge; @GetMapping("/config") public Map<String, Object> getConfig() { Map<String, Object> config = new HashMap<>(); config.put("userName", userName); config.put("userAge", userAge); return config; }}健康检查与监控management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always health: circuitbreakers: enabled: true@Componentpublic class CustomHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); if (errorCode != 0) { return Health.down() .withDetail("Error Code", errorCode) .build(); } return Health.up().build(); } private int check() { // 自定义健康检查逻辑 return 0; }}最佳实践1. 服务命名规范spring: application: # 使用小写,用横线分隔 name: order-service2. 版本管理# 通过元数据区分版本spring: cloud: nacos: discovery: metadata: version: v2 region: beijing3. 优雅下线@Componentpublic class GracefulShutdown implements ApplicationListener<ContextClosedEvent> { @Autowired private NacosRegistration registration; @Override public void onApplicationEvent(ContextClosedEvent event) { // 从注册中心注销 registration.deregister(); // 等待正在处理的请求完成 try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}总结| 注册中心 | 推荐场景 | 优势 | 劣势 ||---------|---------|------|------|| Eureka | Spring Cloud 传统项目 | 成熟稳定 | 停止维护 || Nacos | 新项目的首选 | 功能全面、国产支持 | 学习成本 || Consul | 多语言环境 | 云原生、功能丰富 | 资源占用 || Zookeeper | 已有 ZK 集群 | 成熟可靠 | 功能单一 |选型建议:新项目:Nacos(功能全面,社区活跃)云原生:Consul遗留项目:Eureka
阅读 0·3月6日 21:58

Spring Boot 中如何整合 MyBatis 进行数据库操作?

Spring Boot 整合 MyBatis 详解为什么要用 MyBatisSQL 可控:手写 SQL,便于优化复杂查询轻量级:相比 JPA,学习成本低灵活:支持动态 SQL、存储过程与 Spring Boot 无缝集成:mybatis-spring-boot-starter 提供自动配置环境准备1. 添加依赖<dependencies> <!-- MyBatis Spring Boot Starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 连接池(可选,Spring Boot 默认使用 HikariCP) --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <!-- 分页插件(可选) --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency></dependencies>2. 配置文件# application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC username: root password: password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 300000 connection-timeout: 20000# MyBatis 配置mybatis: # Mapper XML 文件位置 mapper-locations: classpath:mapper/*.xml # 实体类包路径(配置别名) type-aliases-package: com.example.entity # 驼峰命名自动映射 configuration: map-underscore-to-camel-case: true # 打印 SQL log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 全局配置文件(可选,与 configuration 互斥) # config-location: classpath:mybatis-config.xml基础使用方式方式一:注解方式(推荐简单场景)实体类@Datapublic class User { private Long id; private String username; private String email; private Integer age; private LocalDateTime createTime;}Mapper 接口@Mapperpublic interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User selectById(Long id); @Select("SELECT * FROM user") List<User> selectAll(); @Insert("INSERT INTO user(username, email, age) VALUES(#{username}, #{email}, #{age})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(User user); @Update("UPDATE user SET username=#{username}, email=#{email}, age=#{age} WHERE id=#{id}") int update(User user); @Delete("DELETE FROM user WHERE id = #{id}") int deleteById(Long id); // 动态 SQL 示例 @Select("<script>" + "SELECT * FROM user " + "<where>" + " <if test='username != null'>AND username LIKE CONCAT('%', #{username}, '%')</if>" + " <if test='age != null'>AND age = #{age}</if>" + "</where>" + "</script>") List<User> selectByCondition(@Param("username") String username, @Param("age") Integer age);}方式二:XML 方式(推荐复杂场景)Mapper 接口@Mapperpublic interface UserMapper { User selectById(Long id); List<User> selectAll(); int insert(User user); int update(User user); int deleteById(Long id); List<User> selectByCondition(UserQueryDTO query);}XML 映射文件(resources/mapper/UserMapper.xml)<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mapper.UserMapper"> <!-- 结果映射 --> <resultMap id="BaseResultMap" type="com.example.entity.User"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="email" property="email"/> <result column="age" property="age"/> <result column="create_time" property="createTime"/> </resultMap> <!-- 查询单条 --> <select id="selectById" resultMap="BaseResultMap"> SELECT * FROM user WHERE id = #{id} </select> <!-- 查询全部 --> <select id="selectAll" resultMap="BaseResultMap"> SELECT * FROM user </select> <!-- 插入 --> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user(username, email, age, create_time) VALUES(#{username}, #{email}, #{age}, NOW()) </insert> <!-- 更新 --> <update id="update"> UPDATE user <set> <if test="username != null">username = #{username},</if> <if test="email != null">email = #{email},</if> <if test="age != null">age = #{age}</if> </set> WHERE id = #{id} </update> <!-- 删除 --> <delete id="deleteById"> DELETE FROM user WHERE id = #{id} </delete> <!-- 动态条件查询 --> <select id="selectByCondition" resultMap="BaseResultMap"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="age != null"> AND age = #{age} </if> <if test="email != null and email != ''"> AND email = #{email} </if> </where> ORDER BY create_time DESC </select> <!-- 批量插入 --> <insert id="batchInsert"> INSERT INTO user(username, email, age, create_time) VALUES <foreach collection="list" item="user" separator=","> (#{user.username}, #{user.email}, #{user.age}, NOW()) </foreach> </insert> <!-- 批量删除 --> <delete id="batchDelete"> DELETE FROM user WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </delete></mapper>高级特性1. 一对一关联查询<resultMap id="UserWithOrderMap" type="com.example.entity.User"> <id column="user_id" property="id"/> <result column="username" property="username"/> <association property="order" javaType="com.example.entity.Order"> <id column="order_id" property="id"/> <result column="order_no" property="orderNo"/> <result column="amount" property="amount"/> </association></resultMap><select id="selectUserWithOrder" resultMap="UserWithOrderMap"> SELECT u.id as user_id, u.username, o.id as order_id, o.order_no, o.amount FROM user u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = #{userId}</select>2. 一对多关联查询<resultMap id="UserWithOrdersMap" type="com.example.entity.User"> <id column="user_id" property="id"/> <result column="username" property="username"/> <collection property="orders" ofType="com.example.entity.Order"> <id column="order_id" property="id"/> <result column="order_no" property="orderNo"/> <result column="amount" property="amount"/> </collection></resultMap><select id="selectUserWithOrders" resultMap="UserWithOrdersMap"> SELECT u.id as user_id, u.username, o.id as order_id, o.order_no, o.amount FROM user u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = #{userId}</select>3. 分页查询使用 PageHelper:@Servicepublic class UserService { @Autowired private UserMapper userMapper; public PageInfo<User> getUserPage(int pageNum, int pageSize) { // 开启分页 PageHelper.startPage(pageNum, pageSize); // 执行查询 List<User> list = userMapper.selectAll(); // 封装分页信息 return new PageInfo<>(list); } public PageInfo<User> searchUsers(UserQueryDTO query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectByCondition(query); return new PageInfo<>(list); }}4. 多数据源配置spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/db1 username: root password: pass1 secondary: jdbc-url: jdbc:mysql://localhost:3306/db2 username: root password: pass2# MyBatis 配置mybatis: mapper-locations: classpath:mapper/**/*.xml@Configuration@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")public class PrimaryDataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "primarySqlSessionFactory") public SqlSessionFactory sqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/primary/*.xml")); return bean.getObject(); } @Bean(name = "primarySqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }}事务管理@Servicepublic class UserService { @Autowired private UserMapper userMapper; @Autowired private OrderMapper orderMapper; @Transactional public void createUserWithOrder(User user, Order order) { // 插入用户 userMapper.insert(user); // 设置订单用户ID order.setUserId(user.getId()); // 插入订单 orderMapper.insert(order); // 模拟异常,测试事务回滚 if (user.getAge() < 0) { throw new IllegalArgumentException("Invalid age"); } } @Transactional(readOnly = true) public User getUserById(Long id) { return userMapper.selectById(id); } @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { userMapper.update(user); }}代码生成器使用 MyBatis Generator 自动生成代码:<!-- pom.xml --><plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.4.2</version></plugin><!-- generatorConfig.xml --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <context id="default" targetRuntime="MyBatis3"> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mydb" userId="root" password="password"/> <javaModelGenerator targetPackage="com.example.entity" targetProject="src/main/java"/> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java"/> <table tableName="user" domainObjectName="User"/> </context></generatorConfiguration>最佳实践1. 项目结构src/main/java/com/example/├── entity/ # 实体类├── mapper/ # Mapper 接口├── service/ # 业务层├── controller/ # 控制层└── dto/ # 数据传输对象src/main/resources/├── mapper/ # XML 映射文件├── application.yml # 配置文件└── mybatis-config.xml # MyBatis 全局配置(可选)2. Mapper 扫描方式方式1:每个 Mapper 加 @Mapper 注解方式2:启动类统一扫描(推荐)@SpringBootApplication@MapperScan("com.example.mapper")public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}3. SQL 编写规范复杂 SQL 使用 XML,简单 SQL 使用注解使用 #{} 防止 SQL 注入,避免使用 ${}大字段使用延迟加载分页查询必须加 ORDER BY4. 性能优化mybatis: configuration: # 开启二级缓存 cache-enabled: true # 延迟加载 lazy-loading-enabled: true # 积极加载 aggressive-lazy-loading: false # 默认执行器 default-executor-type: reuse常见问题Q1: Invalid bound statement (not found)检查 Mapper 接口和 XML 的 namespace 是否一致检查方法名是否匹配确认 XML 文件在编译后的 target 目录中Q2: 如何打印 SQL 日志?mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 或使用日志框架logging: level: com.example.mapper: debugQ3: 如何处理枚举类型?public enum Status { ACTIVE, INACTIVE;}// 自定义 TypeHandler@MappedTypes(Status.class)public class StatusTypeHandler extends BaseTypeHandler<Status> { // 实现方法}总结| 特性 | 注解方式 | XML 方式 ||------|---------|---------|| 简单 SQL | ✅ 推荐 | 可用 || 复杂 SQL | 繁琐 | ✅ 推荐 || 动态 SQL | 支持(@SelectProvider) | ✅ 更强大 || 维护性 | 一般 | ✅ 好 |Spring Boot + MyBatis 的组合提供了灵活的数据访问能力,适合需要精细控制 SQL 的场景。
阅读 0·3月6日 21:58

Spring Boot 中如何实现缓存?

Spring Boot 缓存详解为什么需要缓存减少数据库压力:热点数据直接从缓存读取提升响应速度:内存访问比磁盘快几个数量级降低系统负载:减少重复计算改善用户体验:页面加载更快Spring Boot 缓存抽象Spring 提供了一套缓存抽象,支持多种缓存实现:ConcurrentMapCache:基于内存,单机使用Caffeine:高性能本地缓存EhCache:老牌缓存框架Redis:分布式缓存Hazelcast:分布式内存数据网格实现方式一:基于内存的 ConcurrentMapCache1. 开启缓存支持@Configuration@EnableCachingpublic class CacheConfig { /** * 配置缓存管理器 */ @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); // 创建多个缓存区域 List<Cache> caches = new ArrayList<>(); caches.add(new ConcurrentMapCache("users")); caches.add(new ConcurrentMapCache("products")); caches.add(new ConcurrentMapCache("orders")); cacheManager.setCaches(caches); return cacheManager; }}2. 使用缓存注解@Service@Slf4jpublic class UserService { @Autowired private UserRepository userRepository; /** * @Cacheable:先查缓存,没有则执行方法并缓存结果 * value:缓存名称 * key:缓存键,支持 SpEL * unless:条件判断,为 true 时不缓存 */ @Cacheable(value = "users", key = "#id", unless = "#result == null") public User getUserById(Long id) { log.info("从数据库查询用户: {}", id); return userRepository.findById(id).orElse(null); } /** * 使用多个 key 组合 */ @Cacheable(value = "users", key = "#username + ':' + #age") public User getUserByUsernameAndAge(String username, Integer age) { log.info("从数据库查询用户: {}, {}", username, age); return userRepository.findByUsernameAndAge(username, age); } /** * @CachePut:先执行方法,再更新缓存 * 适用于更新操作 */ @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { log.info("更新用户: {}", user.getId()); return userRepository.save(user); } /** * @CacheEvict:删除缓存 * allEntries = true:清空整个缓存区域 * beforeInvocation = true:方法执行前删除缓存 */ @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { log.info("删除用户: {}", id); userRepository.deleteById(id); } /** * 清空整个 users 缓存 */ @CacheEvict(value = "users", allEntries = true) public void clearUserCache() { log.info("清空用户缓存"); } /** * @Caching:组合多个缓存操作 */ @Caching( put = { @CachePut(value = "users", key = "#user.id"), @CachePut(value = "users", key = "#user.username") }, evict = { @CacheEvict(value = "users", key = "'list'") } ) public User saveUser(User user) { log.info("保存用户: {}", user.getId()); return userRepository.save(user); }}实现方式二:Caffeine 高性能本地缓存1. 添加依赖<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency>2. 配置 Caffeine@Configuration@EnableCachingpublic class CaffeineCacheConfig { /** * Caffeine 缓存配置 */ @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 配置默认的 Caffeine 规格 cacheManager.setCaffeine(Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大容量 .maximumSize(1000) // 写入后 10 分钟过期 .expireAfterWrite(10, TimeUnit.MINUTES) // 访问后 5 分钟过期 .expireAfterAccess(5, TimeUnit.MINUTES) // 记录缓存统计 .recordStats() ); // 指定缓存名称 cacheManager.setCacheNames(Arrays.asList("users", "products", "orders")); return cacheManager; } /** * 为不同缓存配置不同策略 */ @Bean("customCacheManager") public CacheManager customCacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 用户缓存:高频访问,长期有效 cacheManager.registerCustomCache("users", Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(30, TimeUnit.MINUTES) .recordStats() .build() ); // 产品缓存:中等频率,短期有效 cacheManager.registerCustomCache("products", Caffeine.newBuilder() .maximumSize(5000) .expireAfterWrite(10, TimeUnit.MINUTES) .build() ); // 订单缓存:低频访问,快速过期 cacheManager.registerCustomCache("orders", Caffeine.newBuilder() .maximumSize(1000) .expireAfterAccess(5, TimeUnit.MINUTES) .build() ); return cacheManager; }}3. YAML 配置方式(Spring Boot 2.7+)spring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=10m实现方式三:Redis 分布式缓存1. 添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency>2. 配置 Redisspring: redis: host: localhost port: 6379 password: database: 0 timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 cache: type: redis redis: time-to-live: 600000 # 10分钟 cache-null-values: true use-key-prefix: true key-prefix: "myapp:"3. 配置 RedisCacheManager@Configuration@EnableCachingpublic class RedisCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { // 默认配置 RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPairn .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); // 为不同缓存配置不同过期时间 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("users", defaultConfig.entryTtl(Duration.ofMinutes(30))); configMap.put("products", defaultConfig.entryTtl(Duration.ofMinutes(10))); configMap.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(5))); configMap.put("shortTerm", defaultConfig.entryTtl(Duration.ofSeconds(60))); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(configMap) .transactionAware() .build(); } /** * 配置 RedisTemplate */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用 Jackson 序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; }}缓存注解详解@Cacheable/** * 缓存查询结果 */@Cacheable( value = "users", // 缓存名称 key = "#id", // 缓存键 condition = "#id > 0", // 条件:id>0 时才缓存 unless = "#result == null" // 结果不为 null 时才缓存)public User getUser(Long id) { return userRepository.findById(id).orElse(null);}/** * 使用 SpEL 表达式 */@Cacheable(value = "users", key = "T(com.example.util.CacheKeyUtil).generateKey('user', #id)")public User getUserWithCustomKey(Long id) { return userRepository.findById(id).orElse(null);}/** * 多条件查询缓存 */@Cacheable(value = "users", key = "#root.methodName + ':' + #username + ':' + #status")public List<User> findByUsernameAndStatus(String username, Integer status) { return userRepository.findByUsernameAndStatus(username, status);}@CachePut/** * 更新缓存(方法执行后缓存) */@CachePut(value = "users", key = "#user.id")public User updateUser(User user) { return userRepository.save(user);}/** * 根据结果动态设置 key */@CachePut(value = "users", key = "#result.id")public User createUser(User user) { return userRepository.save(user);}@CacheEvict/** * 删除指定缓存 */@CacheEvict(value = "users", key = "#id")public void deleteUser(Long id) { userRepository.deleteById(id);}/** * 方法执行前删除缓存 */@CacheEvict(value = "users", key = "#id", beforeInvocation = true)public void deleteUserBefore(Long id) { userRepository.deleteById(id);}/** * 清空整个缓存区域 */@CacheEvict(value = "users", allEntries = true)public void clearAllUsers() { // 清空所有用户缓存}/** * 同时清除多个缓存 */@Caching(evict = { @CacheEvict(value = "users", key = "#user.id"), @CacheEvict(value = "users", key = "#user.username"), @CacheEvict(value = "userList", allEntries = true)})public void deleteUserAndClearCache(User user) { userRepository.delete(user);}自定义缓存 Key 生成器@Componentpublic class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()).append(":"); sb.append(method.getName()).append(":"); for (Object param : params) { if (param != null) { sb.append(param.toString()).append(":"); } } return sb.toString(); }}// 使用@Cacheable(value = "users", keyGenerator = "customKeyGenerator")public User getUser(Long id) { return userRepository.findById(id).orElse(null);}缓存条件判断@Servicepublic class ProductService { /** * 只有 VIP 用户的查询结果才缓存 */ @Cacheable( value = "products", key = "#userId", condition = "@userService.isVip(#userId)" ) public List<Product> getRecommendedProducts(Long userId) { // 获取推荐商品 return productRepository.findRecommendedByUserId(userId); } /** * 结果为空时不缓存 */ @Cacheable( value = "products", key = "#id", unless = "#result == null or #result.stock <= 0" ) public Product getProduct(Long id) { return productRepository.findById(id).orElse(null); }}缓存与事务@Servicepublic class OrderService { /** * 缓存与事务结合 * 注意:@Cacheable 在事务之外执行 */ @Transactional @Cacheable(value = "orders", key = "#orderId") public Order getOrder(Long orderId) { return orderRepository.findById(orderId).orElse(null); } /** * 更新操作:先更新数据库,再更新缓存 */ @Transactional @CachePut(value = "orders", key = "#order.id") public Order updateOrder(Order order) { // 业务校验 validateOrder(order); // 更新数据库 Order saved = orderRepository.save(order); // 发送消息 eventPublisher.publishEvent(new OrderUpdatedEvent(saved)); return saved; }}缓存监控@Component@Slf4jpublic class CacheMonitor { @Autowired private CacheManager cacheManager; @Scheduled(fixedRate = 60000) // 每分钟执行 public void logCacheStats() { if (cacheManager instanceof CaffeineCacheManager) { Collection<String> cacheNames = cacheManager.getCacheNames(); for (String cacheName : cacheNames) { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof CaffeineCache) { com.github.benmanes.caffeine.cache.Cache nativeCache = (com.github.benmanes.caffeine.cache.Cache) cache.getNativeCache(); log.info("Cache [{}] Stats: {}", cacheName, nativeCache.stats()); } } } }}缓存穿透、击穿、雪崩解决方案@Servicepublic class CacheSolutionService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; /** * 解决缓存穿透:缓存空值 */ @Cacheable( value = "users", key = "#id", unless = "#result == null" ) public User getUserWithNullCache(Long id) { User user = userRepository.findById(id).orElse(null); // 数据库不存在也缓存空值(短时间) if (user == null) { redisTemplate.opsForValue().set( "users::" + id, "null", 5, TimeUnit.MINUTES ); } return user; } /** * 解决缓存击穿:互斥锁 */ public User getUserWithLock(Long id) { String cacheKey = "users::" + id; String lockKey = "lock:users:" + id; // 先查缓存 String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } // 获取分布式锁 RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁,最多等待 10 秒 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (locked) { try { // 双重检查 cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } // 查询数据库 User user = userRepository.findById(id).orElse(null); // 写入缓存 if (user != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES ); } return user; } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; } /** * 解决缓存雪崩:随机过期时间 */ @Cacheable(value = "products", key = "#categoryId") public List<Product> getProductsByCategory(Long categoryId) { List<Product> products = productRepository.findByCategoryId(categoryId); // 设置随机过期时间,防止同时过期 int randomExpire = 600 + (int)(Math.random() * 300); // 10-15 分钟 redisTemplate.opsForValue().set( "products::" + categoryId, JSON.toJSONString(products), randomExpire, TimeUnit.SECONDS ); return products; }}总结| 缓存类型 | 适用场景 | 优点 | 缺点 ||---------|---------|------|------|| ConcurrentMapCache | 单机、开发测试 | 简单、无需额外依赖 | 不支持分布式 || Caffeine | 单机高性能 | 性能极高、功能丰富 | 不支持分布式 || Redis | 分布式系统 | 支持分布式、持久化 | 网络开销 || EhCache | 企业级应用 | 功能全面、支持集群 | 配置复杂 |选择建议:单机应用:Caffeine分布式应用:Redis简单场景:ConcurrentMapCache
阅读 0·3月6日 21:58

什么是 Spring Boot 的自动配置原理?

Spring Boot 自动配置原理详解核心概念Spring Boot 的自动配置(Auto-Configuration)是其最核心的特性之一,它通过约定优于配置的理念,大大简化了 Spring 应用的开发和配置工作。自动配置的工作原理1. @SpringBootApplication 注解这是自动配置的入口,它组合了三个重要注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic @interface SpringBootApplication {}其中 @EnableAutoConfiguration 是开启自动配置的关键。2. @EnableAutoConfiguration 机制@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {}AutoConfigurationImportSelector 是实现自动配置的核心类。3. 自动配置的核心流程步骤一:读取 META-INF/spring.factoriesSpring Boot 启动时会扫描所有 jar 包中的 META-INF/spring.factories 文件,查找 EnableAutoConfiguration 对应的配置类:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration步骤二:条件注解过滤Spring Boot 使用一系列条件注解(@Conditional)来决定是否加载某个配置类:| 条件注解 | 作用 ||---------|------|| @ConditionalOnClass | 类路径存在指定类时才生效 || @ConditionalOnMissingClass | 类路径不存在指定类时才生效 || @ConditionalOnBean | 容器中存在指定 Bean 时才生效 || @ConditionalOnMissingBean | 容器中不存在指定 Bean 时才生效 || @ConditionalOnProperty | 指定属性满足条件时才生效 || @ConditionalOnWebApplication | 是 Web 应用时才生效 |步骤三:配置属性绑定通过 @EnableConfigurationProperties 将配置文件中的属性绑定到配置类:@Configuration@ConditionalOnClass(DataSource.class)@EnableConfigurationProperties(DataSourceProperties.class)public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public DataSource dataSource(DataSourceProperties properties) { return DataSourceBuilder.create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); }}自动配置的执行时机SpringApplication.run() └── refreshContext() └── invokeBeanFactoryPostProcessors() └── ConfigurationClassPostProcessor └── AutoConfigurationImportSelector.selectImports() └── 加载 spring.factories 中的配置类如何自定义自动配置1. 创建自动配置类@Configuration@ConditionalOnClass(MyService.class)@EnableConfigurationProperties(MyProperties.class)public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService(MyProperties properties) { return new MyService(properties); }}2. 创建属性配置类@ConfigurationProperties(prefix = "my.service")public class MyProperties { private String name; private boolean enabled = true; // getters and setters}3. 注册到 spring.factories在 META-INF/spring.factories 中添加:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.MyAutoConfiguration自动配置的排除与定制排除特定自动配置@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})public class Application {}或在配置文件中:spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration总结Spring Boot 自动配置的核心机制:SPI 机制:通过 spring.factories 文件发现配置类条件装配:使用 @Conditional 系列注解智能判断是否加载配置属性绑定:通过 @ConfigurationProperties 实现外部化配置约定优于配置:提供合理的默认值,减少显式配置这种设计使得 Spring Boot 应用可以快速启动,同时保持高度的可定制性。
阅读 0·3月6日 21:58

WebGL 中的矩阵变换有哪些?MVP 矩阵是什么?

WebGL 中的矩阵变换在 3D 图形渲染中,矩阵变换用于将顶点从一个坐标空间转换到另一个坐标空间。WebGL 使用 4×4 矩阵进行各种变换操作。基本变换矩阵1. 平移矩阵(Translation Matrix)将物体沿 X、Y、Z 轴移动| 1 0 0 tx || 0 1 0 ty || 0 0 1 tz || 0 0 0 1 |function createTranslationMatrix(tx, ty, tz) { return new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 ]);}2. 缩放矩阵(Scale Matrix)沿各轴缩放物体大小| sx 0 0 0 || 0 sy 0 0 || 0 0 sz 0 || 0 0 0 1 |3. 旋转矩阵(Rotation Matrix)绕 X 轴旋转| 1 0 0 0 || 0 cosθ -sinθ 0 || 0 sinθ cosθ 0 || 0 0 0 1 |绕 Y 轴旋转| cosθ 0 sinθ 0 || 0 1 0 0 || -sinθ 0 cosθ 0 || 0 0 0 1 |绕 Z 轴旋转| cosθ -sinθ 0 0 || sinθ cosθ 0 0 || 0 0 1 0 || 0 0 0 1 |MVP 矩阵详解MVP 矩阵是三个矩阵的乘积,用于将顶点从模型空间转换到裁剪空间:MVP = P × V × MM - 模型矩阵(Model Matrix)作用:将顶点从模型空间(局部空间)转换到世界空间// 模型矩阵 = 平移 × 旋转 × 缩放const modelMatrix = mat4.create();mat4.translate(modelMatrix, modelMatrix, [x, y, z]);mat4.rotateX(modelMatrix, modelMatrix, angleX);mat4.rotateY(modelMatrix, modelMatrix, angleY);mat4.scale(modelMatrix, modelMatrix, [sx, sy, sz]);应用场景:物体在世界中的位置物体的旋转角度物体的大小缩放V - 视图矩阵(View Matrix)作用:将顶点从世界空间转换到相机空间(观察空间)// 使用 lookAt 函数创建视图矩阵const viewMatrix = mat4.create();mat4.lookAt( viewMatrix, [0, 0, 5], // 相机位置(眼睛) [0, 0, 0], // 观察目标点 [0, 1, 0] // 上方向向量);视图矩阵的本质:将世界坐标系原点移动到相机位置旋转坐标系使相机朝向 -Z 方向相机位于原点,看向 -Z 轴方向P - 投影矩阵(Projection Matrix)作用:将顶点从相机空间转换到裁剪空间透视投影(Perspective Projection)模拟人眼视觉效果,近大远小const projectionMatrix = mat4.create();mat4.perspective( projectionMatrix, Math.PI / 4, // 视野角度(FOV) canvas.width / canvas.height, // 宽高比 0.1, // 近平面 100.0 // 远平面);正交投影(Orthographic Projection)保持物体大小不变,常用于 2D 游戏或 CAD 软件mat4.ortho( projectionMatrix, -2, 2, // 左右 -2, 2, // 下上 0.1, 100 // 近远);坐标空间转换流程模型空间(局部空间) ↓ [模型矩阵 M]世界空间 ↓ [视图矩阵 V]相机空间(观察空间) ↓ [投影矩阵 P]裁剪空间 ↓ [透视除法]标准化设备坐标(NDC) ↓ [视口变换]屏幕空间顶点着色器中的 MVP 应用// 顶点着色器attribute vec3 a_position;attribute vec3 a_color;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_color;void main() { // 方法1:分别应用三个矩阵 // vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); // vec4 viewPos = u_viewMatrix * worldPos; // gl_Position = u_projectionMatrix * viewPos; // 方法2:使用预计算的 MVP 矩阵(推荐) mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); v_color = a_color;}JavaScript 中的矩阵计算// 使用 gl-matrix 库import { mat4, vec3 } from 'gl-matrix';class Camera { constructor() { this.projectionMatrix = mat4.create(); this.viewMatrix = mat4.create(); this.mvpMatrix = mat4.create(); } setPerspective(fov, aspect, near, far) { mat4.perspective(this.projectionMatrix, fov, aspect, near, far); } lookAt(eye, center, up) { mat4.lookAt(this.viewMatrix, eye, center, up); } getMVPMatrix(modelMatrix) { // MVP = P × V × M mat4.multiply(this.mvpMatrix, this.viewMatrix, modelMatrix); mat4.multiply(this.mvpMatrix, this.projectionMatrix, this.mvpMatrix); return this.mvpMatrix; }}// 使用示例const camera = new Camera();camera.setPerspective(Math.PI / 4, 16/9, 0.1, 100);camera.lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]);const modelMatrix = mat4.create();mat4.translate(modelMatrix, modelMatrix, [1, 0, 0]);const mvp = camera.getMVPMatrix(modelMatrix);gl.uniformMatrix4fv(mvpLocation, false, mvp);常见问题与注意事项1. 矩阵乘法顺序矩阵乘法不满足交换律,顺序非常重要:正确:MVP = P × V × M,应用于顶点:MVP × 顶点先应用的变换在乘法链的右侧2. 行主序 vs 列主序WebGL 使用列主序存储矩阵gl.uniformMatrix4fv 的第三个参数 transpose 必须为 false3. 齐次坐标使用 4D 向量 (x, y, z, w):顶点位置:w = 1方向向量:w = 0(不受平移影响)4. 性能优化在 CPU 端预计算 MVP 矩阵,而不是在着色器中分别相乘使用 uniform 传递预计算的矩阵
阅读 0·3月6日 21:58

WebGL 中的光照模型有哪些?如何实现 Phong 光照模型?

WebGL 光照模型概述光照模型用于模拟光线与物体表面的交互,是 3D 渲染中实现真实感的关键技术。WebGL 中常用的光照模型包括:环境光(Ambient)、漫反射(Diffuse)和镜面反射(Specular)。基本光照模型1. 环境光(Ambient Lighting)模拟场景中无处不在的间接光照,不考虑光源位置和方向。vec3 ambient = ambientStrength * lightColor;2. 漫反射(Diffuse Lighting)模拟光线照射到粗糙表面后向各个方向均匀反射的效果。遵循朗伯余弦定律。// 计算光线方向与法线的夹角float diff = max(dot(normal, lightDir), 0.0);vec3 diffuse = diff * lightColor;3. 镜面反射(Specular Lighting)模拟光线在光滑表面的定向反射,产生高光效果。// Phong 模型vec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);vec3 specular = specularStrength * spec * lightColor;// Blinn-Phong 模型(更高效)vec3 halfwayDir = normalize(lightDir + viewDir);float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);vec3 specular = specularStrength * spec * lightColor;Phong 光照模型详解Phong 光照模型是三种光照成分的组合:最终颜色 = 环境光 + 漫反射 + 镜面反射顶点着色器attribute vec3 a_position;attribute vec3 a_normal;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;uniform mat3 u_normalMatrix; // 法线矩阵(用于正确变换法线)uniform vec3 u_lightPosition;uniform vec3 u_cameraPosition;varying vec3 v_normal;varying vec3 v_lightDir;varying vec3 v_viewDir;varying vec2 v_texCoord;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPos; // 变换法线到世界空间 v_normal = normalize(u_normalMatrix * a_normal); // 计算光线方向(从片段指向光源) v_lightDir = normalize(u_lightPosition - worldPos.xyz); // 计算视线方向(从片段指向相机) v_viewDir = normalize(u_cameraPosition - worldPos.xyz); v_texCoord = a_texCoord;}片段着色器(Phong 模型)precision mediump float;varying vec3 v_normal;varying vec3 v_lightDir;varying vec3 v_viewDir;varying vec2 v_texCoord;uniform vec3 u_lightColor;uniform vec3 u_ambientColor;uniform sampler2D u_diffuseMap;uniform float u_shininess;uniform float u_ambientStrength;uniform float u_specularStrength;void main() { vec3 normal = normalize(v_normal); vec3 lightDir = normalize(v_lightDir); vec3 viewDir = normalize(v_viewDir); // 环境光 vec3 ambient = u_ambientStrength * u_ambientColor; // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 镜面反射(Phong) vec3 reflectDir = reflect(-lightDir, normal); float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * u_lightColor; // 采样纹理 vec4 texColor = texture2D(u_diffuseMap, v_texCoord); // 组合光照 vec3 lighting = ambient + diffuse + specular; vec3 result = lighting * texColor.rgb; gl_FragColor = vec4(result, texColor.a);}片段着色器(Blinn-Phong 模型)Blinn-Phong 是 Phong 的改进版本,使用半角向量代替反射向量,计算更高效。void main() { vec3 normal = normalize(v_normal); vec3 lightDir = normalize(v_lightDir); vec3 viewDir = normalize(v_viewDir); // 环境光 vec3 ambient = u_ambientStrength * u_ambientColor; // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 镜面反射(Blinn-Phong) vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(normal, halfwayDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * u_lightColor; // 组合光照 vec3 lighting = ambient + diffuse + specular; vec3 result = lighting * texture2D(u_diffuseMap, v_texCoord).rgb; gl_FragColor = vec4(result, 1.0);}法线矩阵的计算// 法线矩阵是模型矩阵的逆转置矩阵的左上 3x3 部分function createNormalMatrix(modelMatrix) { const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelMatrix); mat4.transpose(normalMatrix, normalMatrix); // 提取 3x3 部分 return new Float32Array([ normalMatrix[0], normalMatrix[1], normalMatrix[2], normalMatrix[4], normalMatrix[5], normalMatrix[6], normalMatrix[8], normalMatrix[9], normalMatrix[10] ]);}// 或使用 gl-matrix 的 mat3const normalMatrix = mat3.create();mat3.fromMat4(normalMatrix, modelMatrix);mat3.invert(normalMatrix, normalMatrix);mat3.transpose(normalMatrix, normalMatrix);多光源处理#define MAX_LIGHTS 4struct Light { vec3 position; vec3 color; float intensity;};uniform Light u_lights[MAX_LIGHTS];uniform int u_numLights;vec3 calculateLighting(vec3 normal, vec3 viewDir, vec3 fragPos) { vec3 result = u_ambientColor * u_ambientStrength; for (int i = 0; i < MAX_LIGHTS; i++) { if (i >= u_numLights) break; Light light = u_lights[i]; vec3 lightDir = normalize(light.position - fragPos); // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * light.color * light.intensity; // 镜面反射 vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(normal, halfwayDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * light.color * light.intensity; // 衰减 float distance = length(light.position - fragPos); float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance); result += (diffuse + specular) * attenuation; } return result;}不同光源类型方向光(Directional Light)// 方向光只有方向,没有位置uniform vec3 u_lightDirection; // 光线方向vec3 lightDir = normalize(-u_lightDirection); // 指向光源float diff = max(dot(normal, lightDir), 0.0);点光源(Point Light)// 点光源有位置,向四面八方发射uniform vec3 u_lightPosition;uniform float u_constant;uniform float u_linear;uniform float u_quadratic;vec3 lightDir = normalize(u_lightPosition - fragPos);float distance = length(u_lightPosition - fragPos);float attenuation = 1.0 / (u_constant + u_linear * distance + u_quadratic * distance * distance);聚光灯(Spot Light)// 聚光灯有位置、方向和角度限制uniform vec3 u_lightPosition;uniform vec3 u_lightDirection;uniform float u_cutOff; // 内切角余弦uniform float u_outerCutOff; // 外切角余弦vec3 lightDir = normalize(u_lightPosition - fragPos);float theta = dot(lightDir, normalize(-u_lightDirection));float epsilon = u_cutOff - u_outerCutOff;float intensity = clamp((theta - u_outerCutOff) / epsilon, 0.0, 1.0);材质属性struct Material { vec3 ambient; vec3 diffuse; vec3 specular; float shininess;};uniform Material u_material;void main() { vec3 ambient = u_light.ambient * u_material.ambient; vec3 diffuse = u_light.diffuse * (diff * u_material.diffuse); vec3 specular = u_light.specular * (spec * u_material.specular);}Gouraud 着色 vs Phong 着色| 特性 | Gouraud 着色 | Phong 着色 ||------|-------------|------------|| 计算位置 | 顶点着色器 | 片段着色器 || 质量 | 较低(插值) | 较高(逐像素) || 性能 | 更快 | 较慢 || 高光 | 可能不准确 | 准确 |// Gouraud 着色(顶点着色器中计算光照)varying vec3 v_lighting;void main() { // 在顶点级别计算光照 vec3 ambient = ...; vec3 diffuse = ...; vec3 specular = ...; v_lighting = ambient + diffuse + specular;}// 片段着色器void main() { gl_FragColor = vec4(v_lighting * texColor, 1.0);}性能优化建议顶点着色器 vs 片段着色器:顶点数 < 片段数时,在顶点着色器计算需要高质量光照时,在片段着色器计算使用 Blinn-Phong:比 Phong 更高效(避免计算 reflect)视觉效果相似限制光源数量:移动端建议 1-2 个光源桌面端建议 4-8 个光源使用延迟渲染:大量光源时使用避免对每个光源遍历所有片段
阅读 0·3月6日 21:57

WebGL 中的后期处理(Post-processing)是如何实现的?

WebGL 后期处理概述后期处理(Post-processing)是在场景渲染完成后,对渲染结果进行图像处理的技术。通过后期处理可以实现各种视觉效果,如模糊、辉光、色调映射、抗锯齿等。后期处理的基本原理后期处理的核心流程:将场景渲染到纹理(离屏渲染)对纹理进行各种图像处理将处理后的结果渲染到屏幕场景渲染 → 颜色纹理 → 后期处理着色器 → 屏幕 ↓ 深度纹理(可选)基本后期处理框架创建后期处理所需的资源class PostProcess { constructor(gl, width, height) { this.gl = gl; this.width = width; this.height = height; // 创建帧缓冲区 this.framebuffer = gl.createFramebuffer(); // 创建颜色纹理 this.colorTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.colorTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 绑定到帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.colorTexture, 0); // 创建深度渲染缓冲区 this.depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 创建全屏四边形 this.quad = this.createFullscreenQuad(); } createFullscreenQuad() { // 返回 VAO 或顶点数据 const vertices = new Float32Array([ // 位置 // 纹理坐标 -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 ]); // 创建 VBO 等... return { vertices, vbo: this.gl.createBuffer() }; } beginScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); } endScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } apply(effectProgram) { this.gl.useProgram(effectProgram); this.gl.activeTexture(this.gl.TEXTURE0); this.gl.bindTexture(this.gl.TEXTURE_2D, this.colorTexture); this.gl.uniform1i(this.gl.getUniformLocation(effectProgram, 'u_texture'), 0); // 绘制全屏四边形 this.drawQuad(); } drawQuad() { // 绑定 VAO 并绘制 this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); }}后期处理顶点着色器attribute vec2 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord;}常用后期处理效果1. 灰度效果(Grayscale)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;void main() { vec4 color = texture2D(u_texture, v_texCoord); // 加权灰度转换 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = vec4(vec3(gray), color.a);}2. 模糊效果(Blur)高斯模糊precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform vec2 u_texelSize; // 1.0 / textureSizeuniform vec2 u_direction; // 模糊方向 (1,0) 或 (0,1)void main() { vec4 color = vec4(0.0); // 5x5 高斯核 float weights[5]; weights[0] = 0.227027; weights[1] = 0.1945946; weights[2] = 0.1216216; weights[3] = 0.054054; weights[4] = 0.016216; // 中心像素 color += texture2D(u_texture, v_texCoord) * weights[0]; // 两侧像素 for (int i = 1; i < 5; i++) { vec2 offset = u_direction * u_texelSize * float(i); color += texture2D(u_texture, v_texCoord + offset) * weights[i]; color += texture2D(u_texture, v_texCoord - offset) * weights[i]; } gl_FragColor = color;}双通道模糊(优化性能)// 先水平模糊horizontalBlurProgram.setUniform('u_direction', [1, 0]);postProcess.apply(horizontalBlurProgram);// 再垂直模糊verticalBlurProgram.setUniform('u_direction', [0, 1]);postProcess.apply(verticalBlurProgram);3. 边缘检测(Edge Detection)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform vec2 u_texelSize;void main() { // Sobel 算子 float kernelX[9]; kernelX[0] = -1.0; kernelX[1] = 0.0; kernelX[2] = 1.0; kernelX[3] = -2.0; kernelX[4] = 0.0; kernelX[5] = 2.0; kernelX[6] = -1.0; kernelX[7] = 0.0; kernelX[8] = 1.0; float kernelY[9]; kernelY[0] = -1.0; kernelY[1] = -2.0; kernelY[2] = -1.0; kernelY[3] = 0.0; kernelY[4] = 0.0; kernelY[5] = 0.0; kernelY[6] = 1.0; kernelY[7] = 2.0; kernelY[8] = 1.0; vec2 offsets[9]; offsets[0] = vec2(-1, -1); offsets[1] = vec2(0, -1); offsets[2] = vec2(1, -1); offsets[3] = vec2(-1, 0); offsets[4] = vec2(0, 0); offsets[5] = vec2(1, 0); offsets[6] = vec2(-1, 1); offsets[7] = vec2(0, 1); offsets[8] = vec2(1, 1); float edgeX = 0.0; float edgeY = 0.0; for (int i = 0; i < 9; i++) { vec2 coord = v_texCoord + offsets[i] * u_texelSize; float gray = dot(texture2D(u_texture, coord).rgb, vec3(0.299, 0.587, 0.114)); edgeX += gray * kernelX[i]; edgeY += gray * kernelY[i]; } float edge = sqrt(edgeX * edgeX + edgeY * edgeY); gl_FragColor = vec4(vec3(edge), 1.0);}4. 辉光效果(Bloom)// 提取高亮部分precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_threshold;void main() { vec4 color = texture2D(u_texture, v_texCoord); float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); if (brightness > u_threshold) { gl_FragColor = color; } else { gl_FragColor = vec4(0.0); }}// 合成辉光precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_sceneTexture;uniform sampler2D u_bloomTexture;uniform float u_bloomIntensity;void main() { vec4 sceneColor = texture2D(u_sceneTexture, v_texCoord); vec4 bloomColor = texture2D(u_bloomTexture, v_texCoord); gl_FragColor = sceneColor + bloomColor * u_bloomIntensity;}5. 色调映射(Tone Mapping)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_exposure;// Reinhard 色调映射vec3 reinhardToneMapping(vec3 color) { return color / (color + vec3(1.0));}// ACES 色调映射vec3 acesToneMapping(vec3 color) { const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14; return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0);}void main() { vec4 color = texture2D(u_texture, v_texCoord); // 曝光调整 vec3 mapped = vec3(1.0) - exp(-color.rgb * u_exposure); // Gamma 校正 mapped = pow(mapped, vec3(1.0 / 2.2)); gl_FragColor = vec4(mapped, color.a);}6. 色彩调整precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_brightness;uniform float u_contrast;uniform float u_saturation;void main() { vec4 color = texture2D(u_texture, v_texCoord); // 亮度 color.rgb += u_brightness; // 对比度 color.rgb = (color.rgb - 0.5) * u_contrast + 0.5; // 饱和度 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); color.rgb = mix(vec3(gray), color.rgb, u_saturation); gl_FragColor = color;}7. 屏幕空间环境光遮蔽(SSAO)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_colorTexture;uniform sampler2D u_depthTexture;uniform sampler2D u_normalTexture;uniform mat4 u_projectionMatrix;uniform mat4 u_viewMatrix;uniform vec2 u_texelSize;uniform vec3 u_samples[64]; // 采样核vec3 getViewPosition(vec2 texCoord) { float depth = texture2D(u_depthTexture, texCoord).r; vec4 clipPos = vec4(texCoord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); vec4 viewPos = inverse(u_projectionMatrix) * clipPos; return viewPos.xyz / viewPos.w;}void main() { vec3 viewPos = getViewPosition(v_texCoord); vec3 normal = texture2D(u_normalTexture, v_texCoord).xyz * 2.0 - 1.0; float occlusion = 0.0; float radius = 0.5; for (int i = 0; i < 64; i++) { // 采样点 vec3 samplePos = viewPos + u_samples[i] * radius; // 投影到屏幕空间 vec4 offset = u_projectionMatrix * vec4(samplePos, 1.0); offset.xyz = offset.xyz / offset.w * 0.5 + 0.5; // 获取采样点深度 float sampleDepth = getViewPosition(offset.xy).z; // 范围检查 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampleDepth)); // 如果采样点深度小于当前点深度,则遮挡 occlusion += (sampleDepth >= samplePos.z ? 1.0 : 0.0) * rangeCheck; } occlusion = 1.0 - (occlusion / 64.0); vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; gl_FragColor = vec4(color * occlusion, 1.0);}多效果链式处理class PostProcessChain { constructor(gl, width, height) { this.gl = gl; // 创建两个帧缓冲区用于乒乓渲染 this.fbo1 = new PostProcess(gl, width, height); this.fbo2 = new PostProcess(gl, width, height); this.effects = []; } addEffect(effect) { this.effects.push(effect); } render(sceneRenderFunc) { // 第一步:渲染场景到 FBO1 this.fbo1.beginScene(); sceneRenderFunc(); this.fbo1.endScene(); let readFBO = this.fbo1; let writeFBO = this.fbo2; // 应用每个效果 for (let effect of this.effects) { writeFBO.beginScene(); effect.apply(readFBO.colorTexture); writeFBO.endScene(); // 交换读写缓冲区 [readFBO, writeFBO] = [writeFBO, readFBO]; } // 最后渲染到屏幕 this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); finalPass.apply(readFBO.colorTexture); }}性能优化建议降低分辨率:后期处理可以使用半分辨率或四分之一分辨率特别适合模糊等效果合并效果:将多个简单效果合并到一个着色器中减少渲染通道使用 Mipmap:模糊效果可以使用 mipmap 快速降采样智能更新:静态场景不需要每帧都进行后期处理可以隔帧更新某些效果移动端优化:减少采样次数使用简单的近似算法
阅读 0·3月6日 21:57

WebGL 中的帧缓冲区(Framebuffer)和离屏渲染是什么?

WebGL 帧缓冲区概述帧缓冲区(Framebuffer)是 WebGL 中用于离屏渲染的目标。与默认的屏幕缓冲区不同,帧缓冲区可以将渲染结果输出到纹理或渲染缓冲区,而不是直接显示在屏幕上。帧缓冲区的组成一个完整的帧缓冲区(FBO)可以包含以下附件:颜色附件(Color Attachment):存储颜色信息,可以是纹理或渲染缓冲区深度附件(Depth Attachment):存储深度值模板附件(Stencil Attachment):存储模板值┌─────────────────────────────────────┐│ Framebuffer │├─────────────────────────────────────┤│ Color Attachment │ Texture/ ││ │ Renderbuffer │├─────────────────────────────────────┤│ Depth Attachment │ Renderbuffer │├─────────────────────────────────────┤│ Stencil Attachment │ Renderbuffer │└─────────────────────────────────────┘创建帧缓冲区基本创建流程// 1. 创建帧缓冲区const framebuffer = gl.createFramebuffer();// 2. 创建颜色附件(纹理)const colorTexture = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D, colorTexture);gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);// 3. 创建深度附件(渲染缓冲区)const depthBuffer = gl.createRenderbuffer();gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);// 4. 绑定帧缓冲区并附加附件gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);// 5. 检查帧缓冲区完整性const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('Framebuffer incomplete:', status);}// 6. 解绑gl.bindFramebuffer(gl.FRAMEBUFFER, null);离屏渲染基本渲染流程function renderToTexture() { // 1. 绑定帧缓冲区(离屏渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 2. 设置视口 gl.viewport(0, 0, framebufferWidth, framebufferHeight); // 3. 清除缓冲区 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 4. 渲染场景 drawScene(); // 5. 解绑帧缓冲区(恢复屏幕渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 6. 恢复视口 gl.viewport(0, 0, canvas.width, canvas.height); // 7. 使用渲染结果(colorTexture 现在包含渲染结果) gl.bindTexture(gl.TEXTURE_2D, colorTexture); drawFullscreenQuad();}纹理 vs 渲染缓冲区| 特性 | 纹理(Texture) | 渲染缓冲区(Renderbuffer) ||------|-----------------|---------------------------|| 可读性 | 可以作为纹理采样 | 不能直接读取 || 性能 | 稍慢 | 更快(针对写入优化) || 用途 | 需要后续处理的颜色附件 | 深度/模板附件 || 灵活性 | 高 | 低 |选择建议颜色附件:通常使用纹理(需要后续采样)深度/模板附件:通常使用渲染缓冲区(性能更好)多重渲染目标(MRT - WebGL 2.0)// WebGL 2.0 支持同时渲染到多个颜色附件const framebuffer = gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);// 创建多个颜色附件const textures = [];for (let i = 0; i < 4; i++) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0 ); textures.push(texture);}// 指定要绘制的附件gl.drawBuffers([ gl.COLOR_ATTACHMENT0, // 输出到纹理 0 gl.COLOR_ATTACHMENT1, // 输出到纹理 1 gl.COLOR_ATTACHMENT2, // 输出到纹理 2 gl.COLOR_ATTACHMENT3 // 输出到纹理 3]);#version 300 eslayout(location = 0) out vec4 gPosition;layout(location = 1) out vec4 gNormal;layout(location = 2) out vec4 gAlbedo;layout(location = 3) out vec4 gMaterial;void main() { gPosition = vec4(worldPos, 1.0); gNormal = vec4(normal, 0.0); gAlbedo = texture(u_diffuseMap, texCoord); gMaterial = vec4(roughness, metallic, ao, 1.0);}常见应用场景1. 后期处理(Post-processing)// 渲染场景到纹理renderToTexture();// 应用后期处理效果applyBloomEffect();applyToneMapping();2. 阴影贴图(Shadow Mapping)// 第一步:从光源视角渲染深度 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer);renderSceneFromLightPerspective();// 第二步:使用深度纹理渲染场景gl.bindFramebuffer(gl.FRAMEBUFFER, null);useShadowMapTexture();renderSceneWithShadows();3. 反射/折射// 渲染环境到立方体贴图for (let face = 0; face < 6; face++) { gl.bindFramebuffer(gl.FRAMEBUFFER, cubeFramebuffer[face]); setupCameraForFace(face); renderEnvironment();}// 使用环境贴图useCubeMapTexture();renderReflectiveObject();4. 延迟渲染(Deferred Rendering)// G-Buffer 渲染 gl.bindFramebuffer(gl.FRAMEBUFFER, gBuffer);gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);renderGeometry();// 光照计算 gl.bindFramebuffer(gl.FRAMEBUFFER, null);useGBufferTextures();applyLighting();帧缓冲区管理类class Framebuffer { constructor(gl, width, height, options = {}) { this.gl = gl; this.width = width; this.height = height; this.framebuffer = gl.createFramebuffer(); this.textures = {}; this.renderbuffers = {}; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); // 创建颜色附件 if (options.color !== false) { this.addColorAttachment(0, options.colorFormat || gl.RGBA); } // 创建深度附件 if (options.depth) { this.addDepthAttachment(options.depthFormat || gl.DEPTH_COMPONENT16); } // 创建模板附件 if (options.stencil) { this.addStencilAttachment(); } // 检查完整性 const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { throw new Error(`Framebuffer incomplete: ${status}`); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); } addColorAttachment(index, internalFormat) { const gl = this.gl; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + index, gl.TEXTURE_2D, texture, 0); this.textures[`color${index}`] = texture; return texture; } addDepthAttachment(internalFormat) { const gl = this.gl; const renderbuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, this.width, this.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); this.renderbuffers.depth = renderbuffer; return renderbuffer; } bind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); } unbind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } getTexture(name = 'color0') { return this.textures[name]; } resize(width, height) { this.width = width; this.height = height; // 重新创建纹理和渲染缓冲区... } destroy() { const gl = this.gl; gl.deleteFramebuffer(this.framebuffer); Object.values(this.textures).forEach(t => gl.deleteTexture(t)); Object.values(this.renderbuffers).forEach(r => gl.deleteRenderbuffer(r)); }}// 使用示例const fbo = new Framebuffer(gl, 1024, 1024, { color: true, depth: true});fbo.bind();renderScene();fbo.unbind();// 使用渲染结果const texture = fbo.getTexture('color0');gl.bindTexture(gl.TEXTURE_2D, texture);性能优化建议复用帧缓冲区:避免频繁创建和销毁使用适当尺寸:不要创建过大的帧缓冲区共享深度缓冲区:多个帧缓冲区可以共享同一个深度缓冲区延迟创建:只在需要时创建帧缓冲区使用渲染缓冲区存储深度:比纹理更高效常见问题帧缓冲区不完整const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);switch (status) { case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: console.error('附件不完整'); break; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: console.error('缺少附件'); break; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: console.error('附件尺寸不匹配'); break; case gl.FRAMEBUFFER_UNSUPPORTED: console.error('不支持的格式组合'); break;}纹理尺寸限制const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);console.log(`Max texture size: ${maxSize}`);console.log(`Max renderbuffer size: ${maxRenderbufferSize}`);
阅读 0·3月6日 21:57

WebGL 性能优化有哪些常用技巧?

WebGL 性能优化概述WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。1. 减少绘制调用(Draw Calls)问题每次 gl.drawArrays 或 gl.drawElements 都有 CPU 到 GPU 的通信开销。优化方案批量绘制(Batching)// 优化前:多次绘制调用for (let mesh of meshes) { gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo); gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);}// 优化后:合并到一个缓冲区const mergedBuffer = mergeMeshes(meshes);gl.bindBuffer(gl.ARRAY_BUFFER, mergedBuffer);gl.drawArrays(gl.TRIANGLES, 0, totalVertexCount);实例化渲染(Instanced Rendering)// WebGL 2.0 原生支持// 一次绘制调用渲染多个相同几何体const instanceCount = 1000;gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);2. 减少状态切换问题频繁切换着色器程序、纹理、缓冲区等状态会造成性能开销。优化方案按状态排序// 按着色器程序排序meshes.sort((a, b) => a.program.id - b.program.id);// 按纹理排序meshes.sort((a, b) => a.texture.id - b.texture.id);let currentProgram = null;let currentTexture = null;for (let mesh of meshes) { // 只在需要时切换程序 if (mesh.program !== currentProgram) { gl.useProgram(mesh.program); currentProgram = mesh.program; } // 只在需要时切换纹理 if (mesh.texture !== currentTexture) { gl.bindTexture(gl.TEXTURE_2D, mesh.texture); currentTexture = mesh.texture; } mesh.draw();}使用纹理图集(Texture Atlas)// 将多个小纹理合并为一个大纹理// 减少纹理绑定切换次数const atlasTexture = createTextureAtlas([ 'texture1.png', 'texture2.png', 'texture3.png']);// 在着色器中使用纹理坐标偏移uniform vec2 u_atlasOffset;uniform vec2 u_atlasScale;vec2 atlasCoord = a_texCoord * u_atlasScale + u_atlasOffset;vec4 color = texture2D(u_texture, atlasCoord);3. 优化着色器顶点着色器优化// 优化前:在顶点着色器中进行复杂计算attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;void main() { // 每次顶点都进行矩阵乘法 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0);}// 优化后:在 CPU 预计算 MVP 矩阵uniform mat4 u_mvpMatrix;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0);}片段着色器优化// 优化前:复杂的逐像素计算void main() { vec3 lightDir = normalize(u_lightPos - v_worldPos); float diff = max(dot(v_normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // ... 更多计算}// 优化后:顶点着色器计算光照// 顶点着色器varying vec3 v_lightIntensity;void main() { // 在顶点级别计算光照 vec3 lightDir = normalize(u_lightPos - worldPos); float diff = max(dot(normal, lightDir), 0.0); v_lightIntensity = diff * u_lightColor;}// 片段着色器void main() { // 使用插值后的光照强度 gl_FragColor = vec4(v_lightIntensity * textureColor, 1.0);}使用适当精度// 高精度(highp)- 顶点位置、变换矩阵attribute highp vec3 a_position;uniform highp mat4 u_mvpMatrix;// 中精度(mediump)- 颜色、纹理坐标attribute mediump vec2 a_texCoord;varying mediump vec2 v_texCoord;// 低精度(lowp)- 光照计算结果varying lowp vec3 v_lightColor;4. 缓冲区优化使用 VAO 减少状态设置// WebGL 2.0 或扩展支持const vao = gl.createVertexArray();gl.bindVertexArray(vao);// 配置顶点属性(只执行一次)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawArrays(gl.TRIANGLES, 0, count);使用索引绘制// 优化前:36 个顶点定义一个立方体const vertices = new Float32Array([ // 每个面 6 个顶点,共 6 个面 // 大量重复顶点数据]);// 优化后:8 个顶点 + 36 个索引const vertices = new Float32Array([ // 8 个唯一顶点]);const indices = new Uint16Array([ // 36 个索引定义 12 个三角形]);gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);使用 Interleaved Arrays// 优化前:分离的缓冲区const positions = new Float32Array([/* ... */]);const colors = new Float32Array([/* ... */]);const texCoords = new Float32Array([/* ... */]);// 优化后:交错的顶点数据const vertices = new Float32Array([ // x, y, z, r, g, b, u, v 0, 0, 0, 1, 0, 0, 0, 0, // 顶点 1 1, 0, 0, 0, 1, 0, 1, 0, // 顶点 2 // ...]);// 更好的缓存局部性5. 纹理优化纹理压缩// 使用压缩纹理格式const compressedExtension = gl.getExtension('WEBGL_compressed_texture_s3tc');// 上传压缩纹理数据gl.compressedTexImage2D( gl.TEXTURE_2D, 0, compressedExtension.COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, compressedData);Mipmap 使用// 启用 mipmap 提高渲染质量和性能gl.generateMipmap(gl.TEXTURE_2D);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);纹理尺寸优化使用 2 的幂次尺寸(支持 mipmap)避免过大的纹理(内存和带宽开销)根据距离使用不同分辨率的纹理(LOD)6. 遮挡剔除和视锥剔除视锥剔除function isInFrustum(boundingBox, viewProjectionMatrix) { // 将包围盒转换到裁剪空间 // 检查是否在视锥体内 const corners = boundingBox.getCorners(); for (let corner of corners) { const clipPos = transformPoint(corner, viewProjectionMatrix); if (Math.abs(clipPos.x) <= clipPos.w && Math.abs(clipPos.y) <= clipPos.w && 0 <= clipPos.z && clipPos.z <= clipPos.w) { return true; } } return false;}// 只渲染在视锥体内的物体for (let object of scene.objects) { if (isInFrustum(object.boundingBox, vpMatrix)) { object.render(); }}遮挡查询(WebGL 2.0)const query = gl.createQuery();// 开始遮挡查询gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);drawBoundingBox(object);gl.endQuery(gl.ANY_SAMPLES_PASSED);// 检查结果const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);if (available) { const visible = gl.getQueryParameter(query, gl.QUERY_RESULT) > 0; if (visible) { drawDetailedMesh(object); }}7. 帧缓冲区优化减少分辨率// 在高 DPI 屏幕上使用适当分辨率const dpr = Math.min(window.devicePixelRatio, 2);canvas.width = canvas.clientWidth * dpr;canvas.height = canvas.clientHeight * dpr;延迟渲染优化// G-Buffer 优化:使用适当精度// 位置:RGB16F 或 RGBA16F// 法线:RGB10_A2 或 RGBA8// 材质:RGBA88. JavaScript 优化避免垃圾回收// 优化前:每帧创建新数组function update() { const matrix = new Float32Array(16); // 创建垃圾 // ...}// 优化后:重用数组const matrix = new Float32Array(16);function update() { // 重用 matrix,不创建新对象 mat4.identity(matrix); // ...}使用 TypedArrays// 使用 Float32Array 而不是普通数组const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);9. 移动端优化减少过度绘制// 从前到后绘制不透明物体gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);opaqueObjects.sort((a, b) => b.distance - a.distance);for (let obj of opaqueObjects) { obj.draw();}使用适当精度// 移动端使用 mediump 优化性能precision mediump float;避免复杂着色器减少纹理采样次数避免动态分支简化光照计算10. 性能监控// 使用 EXT_disjoint_timer_query 测量 GPU 时间const ext = gl.getExtension('EXT_disjoint_timer_query');const query = ext.createQueryEXT();ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);drawScene();ext.endQueryEXT(ext.TIME_ELAPSED_EXT);// 获取结果const available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);if (available) { const timeElapsed = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT); console.log(`GPU time: ${timeElapsed / 1000000} ms`);}总结| 优化方向 | 主要技巧 ||----------|----------|| 绘制调用 | 批量绘制、实例化渲染 || 状态切换 | 状态排序、纹理图集、VAO || 着色器 | 预计算、适当精度、简化计算 || 缓冲区 | 索引绘制、交错数组 || 纹理 | 压缩、mipmap、合理尺寸 || 剔除 | 视锥剔除、遮挡查询 || JavaScript | 避免 GC、TypedArrays |
阅读 0·3月6日 21:57