当前位置: 首页 > news >正文

Spring MVC 多租户架构与数据隔离教程

文章目录

    • 目录
    • 多租户架构概述
      • 多租户架构定义
      • 多租户架构优势
      • 多租户架构模式
    • 多租户数据隔离策略
      • 数据库级隔离实现
      • 表级隔离实现
      • 行级隔离实现
    • 租户识别与路由机制
      • 子域名识别
      • URL路径识别
      • 请求头识别
      • 租户拦截器
    • Spring MVC多租户实现
      • 租户配置管理
      • 多租户控制器
      • 租户数据源路由
    • 数据访问层多租户设计
      • 租户感知Repository
      • 租户数据过滤器
      • 多租户事务管理
    • 缓存策略与租户隔离
      • 租户缓存配置
      • 租户缓存键策略
      • 缓存服务实现
    • 安全与权限控制
      • 租户权限管理
      • 租户安全拦截器
    • 性能优化与监控
      • 租户性能监控
      • 租户资源限制
      • 租户缓存预热
    • 最佳实践
      • 1. 租户隔离最佳实践
      • 2. 性能优化最佳实践
      • 3. 安全最佳实践
    • 常见问题解决
      • 1. 租户上下文丢失
      • 2. 缓存污染
      • 3. 事务回滚问题
    • 总结

目录

  1. 多租户架构概述
  2. 多租户数据隔离策略
  3. 租户识别与路由机制
  4. Spring MVC多租户实现
  5. 数据访问层多租户设计
  6. 缓存策略与租户隔离
  7. 安全与权限控制
  8. 性能优化与监控
  9. 最佳实践
  10. 常见问题解决
  11. 总结

多租户架构概述

多租户架构定义

多租户架构(Multi-Tenancy)是一种软件架构模式,其中单个应用程序实例为多个租户(客户、组织或用户组)提供服务,同时确保租户之间的数据隔离和安全性。

多租户架构优势

1. 成本效益

  • 共享基础设施,降低运营成本
  • 统一维护和升级
  • 资源利用率高

2. 可扩展性

  • 支持大量租户
  • 弹性扩展能力
  • 快速部署新租户

3. 维护便利

  • 统一代码库
  • 集中式管理
  • 标准化流程

多租户架构模式

1. 数据库级隔离(Database per Tenant)

租户A → 数据库A
租户B → 数据库B
租户C → 数据库C

2. 表级隔离(Schema per Tenant)

数据库
├── tenant_a_schema
├── tenant_b_schema
└── tenant_c_schema

3. 行级隔离(Row Level Security)

用户表
├── id, name, tenant_id
├── 1, Alice, tenant_a
├── 2, Bob, tenant_b
└── 3, Carol, tenant_a

多租户数据隔离策略

数据库级隔离实现

Maven依赖配置

<dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Data JPA --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- 多数据源支持 --><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.2</version></dependency><!-- 租户上下文 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency>
</dependencies>

多数据源配置

@Configuration
@EnableTransactionManagement
public class MultiTenantDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.tenant")public DataSource tenantDataSource() {return DataSourceBuilder.create().build();}@Beanpublic DataSource routingDataSource() {DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();Map<Object, Object> dataSourceMap = new HashMap<>();dataSourceMap.put("master", masterDataSource());dataSourceMap.put("tenant", tenantDataSource());routingDataSource.setDefaultTargetDataSource(masterDataSource());routingDataSource.setTargetDataSources(dataSourceMap);return routingDataSource;}
}

表级隔离实现

租户Schema管理

@Component
public class TenantSchemaManager {@Autowiredprivate JdbcTemplate jdbcTemplate;public void createTenantSchema(String tenantId) {String schemaName = "tenant_" + tenantId;// 创建租户SchemajdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);// 创建租户表createTenantTables(schemaName);}private void createTenantTables(String schemaName) {String createUserTable = """CREATE TABLE IF NOT EXISTS %s.users (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) NOT NULL,email VARCHAR(100) NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)""".formatted(schemaName);jdbcTemplate.execute(createUserTable);}public void switchToTenantSchema(String tenantId) {String schemaName = "tenant_" + tenantId;jdbcTemplate.execute("USE " + schemaName);}
}

行级隔离实现

租户上下文管理

public class TenantContext {private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();public static void setCurrentTenant(String tenantId) {currentTenant.set(tenantId);}public static String getCurrentTenant() {return currentTenant.get();}public static void clear() {currentTenant.remove();}
}

租户实体基类

@MappedSuperclass
public abstract class TenantAwareEntity {@Column(name = "tenant_id", nullable = false)private String tenantId;@PrePersistprotected void onCreate() {if (tenantId == null) {tenantId = TenantContext.getCurrentTenant();}}// getter and setterpublic String getTenantId() {return tenantId;}public void setTenantId(String tenantId) {this.tenantId = tenantId;}
}

租户识别与路由机制

子域名识别

租户解析器

@Component
public class SubdomainTenantResolver implements TenantResolver {@Overridepublic String resolveTenant(HttpServletRequest request) {String serverName = request.getServerName();// 解析子域名:tenant1.example.com -> tenant1if (serverName.contains(".")) {String[] parts = serverName.split("\\.");if (parts.length >= 3) {return parts[0];}}return "default";}
}

URL路径识别

路径租户解析器

@Component
public class PathTenantResolver implements TenantResolver {@Overridepublic String resolveTenant(HttpServletRequest request) {String requestURI = request.getRequestURI();// 解析路径:/tenant/tenant1/users -> tenant1Pattern pattern = Pattern.compile("^/tenant/([^/]+)/");Matcher matcher = pattern.matcher(requestURI);if (matcher.find()) {return matcher.group(1);}return "default";}
}

请求头识别

Header租户解析器

@Component
public class HeaderTenantResolver implements TenantResolver {private static final String TENANT_HEADER = "X-Tenant-ID";@Overridepublic String resolveTenant(HttpServletRequest request) {String tenantId = request.getHeader(TENANT_HEADER);return tenantId != null ? tenantId : "default";}
}

租户拦截器

租户上下文拦截器

@Component
public class TenantContextInterceptor implements HandlerInterceptor {@Autowiredprivate List<TenantResolver> tenantResolvers;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String tenantId = resolveTenant(request);if (tenantId == null || tenantId.isEmpty()) {response.setStatus(HttpStatus.BAD_REQUEST.value());response.getWriter().write("Tenant ID is required");return false;}// 验证租户是否存在if (!isValidTenant(tenantId)) {response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("Invalid tenant ID");return false;}TenantContext.setCurrentTenant(tenantId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContext.clear();}private String resolveTenant(HttpServletRequest request) {for (TenantResolver resolver : tenantResolvers) {String tenantId = resolver.resolveTenant(request);if (tenantId != null && !tenantId.equals("default")) {return tenantId;}}return "default";}private boolean isValidTenant(String tenantId) {// 实现租户验证逻辑return tenantService.exists(tenantId);}
}

Spring MVC多租户实现

租户配置管理

租户配置实体

@Entity
@Table(name = "tenant_configs")
public class TenantConfig {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "tenant_id", nullable = false)private String tenantId;@Column(name = "config_key", nullable = false)private String configKey;@Column(name = "config_value")private String configValue;@Column(name = "config_type")private String configType;// constructors, getters, setters
}

租户配置服务

@Service
public class TenantConfigService {@Autowiredprivate TenantConfigRepository configRepository;public String getConfig(String key, String defaultValue) {String tenantId = TenantContext.getCurrentTenant();Optional<TenantConfig> config = configRepository.findByTenantIdAndConfigKey(tenantId, key);return config.map(TenantConfig::getConfigValue).orElse(defaultValue);}public void setConfig(String key, String value) {String tenantId = TenantContext.getCurrentTenant();TenantConfig config = configRepository.findByTenantIdAndConfigKey(tenantId, key).orElse(new TenantConfig());config.setTenantId(tenantId);config.setConfigKey(key);config.setConfigValue(value);configRepository.save(config);}
}

多租户控制器

租户感知控制器

@RestController
@RequestMapping("/api/tenant")
public class TenantAwareController {@Autowiredprivate UserService userService;@Autowiredprivate TenantConfigService configService;@GetMapping("/users")public ResponseEntity<List<User>> getUsers() {// 自动使用当前租户上下文List<User> users = userService.findAll();return ResponseEntity.ok(users);}@PostMapping("/users")public ResponseEntity<User> createUser(@RequestBody User user) {// 租户ID会自动设置User savedUser = userService.save(user);return ResponseEntity.ok(savedUser);}@GetMapping("/config/{key}")public ResponseEntity<String> getConfig(@PathVariable String key) {String value = configService.getConfig(key, "");return ResponseEntity.ok(value);}
}

租户数据源路由

动态数据源路由

@Component
public class TenantDataSourceRouter {@Autowiredprivate DataSource masterDataSource;private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();public DataSource getDataSource(String tenantId) {if ("master".equals(tenantId)) {return masterDataSource;}return tenantDataSources.computeIfAbsent(tenantId, this::createTenantDataSource);}private DataSource createTenantDataSource(String tenantId) {// 根据租户ID创建数据源HikariConfig config = new HikariConfig();config.setJdbcUrl("jdbc:mysql://localhost:3306/tenant_" + tenantId);config.setUsername("tenant_user");config.setPassword("tenant_password");return new HikariDataSource(config);}
}

数据访问层多租户设计

租户感知Repository

基础租户Repository

@NoRepositoryBean
public interface TenantAwareRepository<T, ID> extends JpaRepository<T, ID> {@Query("SELECT e FROM #{#entityName} e WHERE e.tenantId = :tenantId")List<T> findByTenantId(@Param("tenantId") String tenantId);@Query("SELECT e FROM #{#entityName} e WHERE e.tenantId = :tenantId AND e.id = :id")Optional<T> findByTenantIdAndId(@Param("tenantId") String tenantId, @Param("id") ID id);@Modifying@Query("DELETE FROM #{#entityName} e WHERE e.tenantId = :tenantId")void deleteByTenantId(@Param("tenantId") String tenantId);
}

用户Repository实现

@Repository
public interface UserRepository extends TenantAwareRepository<User, Long> {List<User> findByTenantIdAndUsernameContaining(String tenantId, String username);@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId AND u.email = :email")Optional<User> findByTenantIdAndEmail(@Param("tenantId") String tenantId, @Param("email") String email);
}

租户数据过滤器

JPA租户过滤器

@Component
public class TenantFilter {@EventListenerpublic void handleTenantFilter(EntityManagerFactory emf) {EntityManager em = emf.createEntityManager();// 设置租户过滤条件String tenantId = TenantContext.getCurrentTenant();if (tenantId != null) {em.setProperty("tenant.id", tenantId);}}
}

Hibernate过滤器配置

@Entity
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User extends TenantAwareEntity {// entity fields
}

多租户事务管理

租户事务管理器

@Configuration
@EnableTransactionManagement
public class TenantTransactionConfig {@Beanpublic PlatformTransactionManager tenantTransactionManager() {return new DataSourceTransactionManager(tenantDataSource());}@Beanpublic TransactionTemplate tenantTransactionTemplate() {return new TransactionTemplate(tenantTransactionManager());}
}

缓存策略与租户隔离

租户缓存配置

Redis租户缓存配置

@Configuration
@EnableCaching
public class TenantCacheConfig {@Beanpublic CacheManager cacheManager(RedisConnectionFactory connectionFactory) {RedisCacheManager.Builder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(cacheConfiguration());return builder.build();}private RedisCacheConfiguration cacheConfiguration() {return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(60)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));}
}

租户缓存键策略

租户缓存键生成器

@Component
public class TenantCacheKeyGenerator implements KeyGenerator {@Overridepublic Object generate(Object target, Method method, Object... params) {String tenantId = TenantContext.getCurrentTenant();String className = target.getClass().getSimpleName();String methodName = method.getName();StringBuilder keyBuilder = new StringBuilder();keyBuilder.append(tenantId).append(":");keyBuilder.append(className).append(":");keyBuilder.append(methodName);for (Object param : params) {keyBuilder.append(":").append(param.toString());}return keyBuilder.toString();}
}

缓存服务实现

租户缓存服务

@Service
public class TenantCacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public void put(String key, Object value) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;redisTemplate.opsForValue().set(tenantKey, value);}public Object get(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;return redisTemplate.opsForValue().get(tenantKey);}public void evict(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;redisTemplate.delete(tenantKey);}public void evictByPattern(String pattern) {String tenantId = TenantContext.getCurrentTenant();String tenantPattern = tenantId + ":" + pattern;Set<String> keys = redisTemplate.keys(tenantPattern);if (!keys.isEmpty()) {redisTemplate.delete(keys);}}
}

安全与权限控制

租户权限管理

租户权限实体

@Entity
@Table(name = "tenant_permissions")
public class TenantPermission {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "tenant_id", nullable = false)private String tenantId;@Column(name = "user_id", nullable = false)private Long userId;@Column(name = "resource", nullable = false)private String resource;@Column(name = "action", nullable = false)private String action;@Column(name = "granted", nullable = false)private Boolean granted;// constructors, getters, setters
}

租户权限服务

@Service
public class TenantPermissionService {@Autowiredprivate TenantPermissionRepository permissionRepository;public boolean hasPermission(String resource, String action) {String tenantId = TenantContext.getCurrentTenant();Long userId = getCurrentUserId();Optional<TenantPermission> permission = permissionRepository.findByTenantIdAndUserIdAndResourceAndAction(tenantId, userId, resource, action);return permission.map(TenantPermission::getGranted).orElse(false);}public void grantPermission(Long userId, String resource, String action) {String tenantId = TenantContext.getCurrentTenant();TenantPermission permission = new TenantPermission();permission.setTenantId(tenantId);permission.setUserId(userId);permission.setResource(resource);permission.setAction(action);permission.setGranted(true);permissionRepository.save(permission);}private Long getCurrentUserId() {// 从安全上下文获取当前用户IDAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {UserDetails userDetails = (UserDetails) authentication.getPrincipal();return Long.valueOf(userDetails.getUsername());}return null;}
}

租户安全拦截器

租户权限拦截器

@Component
public class TenantSecurityInterceptor implements HandlerInterceptor {@Autowiredprivate TenantPermissionService permissionService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String tenantId = TenantContext.getCurrentTenant();String resource = getResourceFromRequest(request);String action = getActionFromRequest(request);if (!permissionService.hasPermission(resource, action)) {response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("Access denied for tenant: " + tenantId);return false;}return true;}private String getResourceFromRequest(HttpServletRequest request) {String requestURI = request.getRequestURI();// 解析资源名称return requestURI.split("/")[2]; // 假设格式为 /api/tenant/{resource}}private String getActionFromRequest(HttpServletRequest request) {String method = request.getMethod();return method.toLowerCase();}
}

性能优化与监控

租户性能监控

租户性能指标

@Component
public class TenantPerformanceMetrics {private final MeterRegistry meterRegistry;private final Map<String, Timer> tenantTimers = new ConcurrentHashMap<>();public TenantPerformanceMetrics(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;}public void recordRequest(String tenantId, String endpoint, Duration duration) {Timer timer = tenantTimers.computeIfAbsent(tenantId, id -> Timer.builder("tenant.request.duration").tag("tenant", id).register(meterRegistry));timer.record(duration);// 记录租户特定指标Counter.builder("tenant.request.count").tag("tenant", tenantId).tag("endpoint", endpoint).register(meterRegistry).increment();}public void recordError(String tenantId, String errorType) {Counter.builder("tenant.error.count").tag("tenant", tenantId).tag("error.type", errorType).register(meterRegistry).increment();}
}

租户资源限制

租户资源限制器

@Component
public class TenantResourceLimiter {private final Map<String, AtomicInteger> tenantRequestCounts = new ConcurrentHashMap<>();private final Map<String, Long> tenantLastReset = new ConcurrentHashMap<>();public boolean isWithinLimits(String tenantId, int maxRequestsPerMinute) {long currentTime = System.currentTimeMillis();long oneMinuteAgo = currentTime - 60000;// 重置计数器if (tenantLastReset.getOrDefault(tenantId, 0L) < oneMinuteAgo) {tenantRequestCounts.put(tenantId, new AtomicInteger(0));tenantLastReset.put(tenantId, currentTime);}AtomicInteger count = tenantRequestCounts.computeIfAbsent(tenantId, k -> new AtomicInteger(0));return count.incrementAndGet() <= maxRequestsPerMinute;}public void recordRequest(String tenantId) {tenantRequestCounts.computeIfAbsent(tenantId, k -> new AtomicInteger(0)).incrementAndGet();}
}

租户缓存预热

租户缓存预热服务

@Service
public class TenantCacheWarmupService {@Autowiredprivate TenantCacheService cacheService;@Autowiredprivate UserService userService;@EventListenerpublic void handleTenantActivation(TenantActivatedEvent event) {String tenantId = event.getTenantId();// 预热用户数据List<User> users = userService.findAll();cacheService.put("users", users);// 预热配置数据Map<String, String> configs = loadTenantConfigs(tenantId);cacheService.put("configs", configs);}private Map<String, String> loadTenantConfigs(String tenantId) {// 加载租户配置return new HashMap<>();}
}

最佳实践

1. 租户隔离最佳实践

数据隔离原则

// ✅ 好的实践:始终检查租户ID
@Service
public class UserService {public User findById(Long id) {String tenantId = TenantContext.getCurrentTenant();return userRepository.findByTenantIdAndId(tenantId, id).orElseThrow(() -> new UserNotFoundException(id));}
}// ❌ 不好的实践:忽略租户隔离
@Service
public class BadUserService {public User findById(Long id) {return userRepository.findById(id)  // 可能返回其他租户的数据.orElseThrow(() -> new UserNotFoundException(id));}
}

2. 性能优化最佳实践

连接池配置

spring:datasource:hikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000idle-timeout: 600000max-lifetime: 1800000

缓存策略

@Service
public class OptimizedUserService {@Cacheable(value = "users", key = "#tenantId + ':' + #id")public User findById(String tenantId, Long id) {return userRepository.findByTenantIdAndId(tenantId, id).orElseThrow(() -> new UserNotFoundException(id));}@CacheEvict(value = "users", key = "#tenantId + ':' + #user.id")public User updateUser(String tenantId, User user) {return userRepository.save(user);}
}

3. 安全最佳实践

租户验证

@Component
public class TenantValidator {public void validateTenantAccess(String tenantId, String resourceId) {String currentTenant = TenantContext.getCurrentTenant();if (!currentTenant.equals(tenantId)) {throw new TenantAccessDeniedException("Access denied: tenant mismatch");}}public void validateResourceOwnership(String tenantId, String resourceId) {// 验证资源是否属于当前租户if (!isResourceOwnedByTenant(resourceId, tenantId)) {throw new ResourceAccessDeniedException("Resource does not belong to tenant");}}
}

常见问题解决

1. 租户上下文丢失

问题描述:异步操作中租户上下文丢失

解决方案

@Service
public class AsyncTenantService {@Asyncpublic CompletableFuture<Void> processAsync(String tenantId) {// 手动设置租户上下文TenantContext.setCurrentTenant(tenantId);try {// 执行业务逻辑doProcess();return CompletableFuture.completedFuture(null);} finally {TenantContext.clear();}}
}

2. 缓存污染

问题描述:不同租户数据在缓存中混合

解决方案

@Component
public class TenantAwareCacheManager {public void put(String key, Object value) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;cacheManager.getCache("default").put(tenantKey, value);}public Object get(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;return cacheManager.getCache("default").get(tenantKey, Object.class);}
}

3. 事务回滚问题

问题描述:多租户事务回滚影响其他租户

解决方案

@Service
@Transactional
public class TenantAwareService {@Transactional(rollbackFor = Exception.class)public void processTenantData(String tenantId) {try {TenantContext.setCurrentTenant(tenantId);// 执行业务逻辑doProcess();} catch (Exception e) {// 只回滚当前租户的数据throw new TenantProcessingException("Failed to process tenant data", e);} finally {TenantContext.clear();}}
}

总结

Spring MVC多租户架构与数据隔离是现代SaaS应用的核心技术。通过合理的设计和实现,可以实现:

核心优势

  • 数据安全:租户间数据完全隔离
  • 性能优化:共享基础设施,降低成本
  • 可扩展性:支持大量租户并发访问
  • 维护便利:统一代码库,集中管理

关键技术点

  • 租户识别与路由机制
  • 数据隔离策略选择
  • 缓存策略与租户隔离
  • 安全与权限控制
  • 性能监控与优化

实施建议

  1. 根据业务需求选择合适的隔离策略
  2. 建立完善的租户管理机制
  3. 实施严格的安全控制
  4. 持续监控和优化性能
  5. 制定详细的运维规范

通过本教程的学习,小伙伴们将掌握Spring MVC多租户架构的设计与实现,为构建企业级SaaS应用奠定坚实基础。

http://www.dtcms.com/a/485973.html

相关文章:

  • MySQL数据库如何实现主从复制
  • 如何在 Docker 中设置环境变量 ?
  • 【C++】STL容器--list的使用
  • 【深度学习计算机视觉】12:风格迁移
  • 网站到期可以续费织梦安装网站后图片
  • 公司购物网站备案wordpress恢复主题
  • C++基于opencv实现的暗通道的先验图像去雾
  • 大型PCB标定方案:基于对角Mark点的分区域识别与校准
  • 做羞羞事视频网站网站策划哪里找
  • 【Android RxJava】Observal与Subject深入理解
  • 基于Rokid CXR-S SDK的智能AR翻译助手技术拆解与实现指南
  • 【uniapp】微信小程序修改按钮样式
  • Lombok使用指南(中)
  • Threejs入门学习笔记
  • 机器学习模型评估指标AUC详解:从理论到实践
  • 凡科建站小程序网站设计的一般流程
  • Linux C/C++ 学习日记(24)UDP协议的介绍:广播、多播的实现
  • OpenHarmony内核基础:LiteOS-M内核与POSIX/CMSIS接口
  • C语言实现Modbus TCP/IP协议客户端-服务器
  • ORACLE 19C ADG环境 如何快速删除1.8TB的分区表?有哪些注意事项?
  • 重庆黔江做防溺水的网站少儿编程十大培训机构
  • 浅谈中兴电子商务网站建设html考试界面设计
  • 工业三防平板背后的条码与RFID采集技术
  • pytorch框架GPU适配npu
  • 【散列函数】哈希函数简介
  • 学英语音标作用,能听出声音拼音组成,记忆效率提高
  • 学习日记day
  • Python爬虫数据可视化:深度分析贝壳成交价格趋势与分布
  • C++中的父继子承(2)多继承菱形继承问题,多继承指针偏移,继承组合分析+高质量习题扫尾继承多态
  • 做公司网站别人能看到吗6网站源码传到服务器上后怎么做