Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 多租户模式配置改进说明

## 问题背景

用户在 issue #3835 中提出了一个架构设计问题:

> 基础 Wx 实现类中已经有 configMap 了,可以用 configMap 来存储不同的小程序配置。不同的配置,都是复用同一个 http 客户端。为什么在各个 spring-boot-starter 中又单独创建类来存储不同的配置?从 spring 的配置来看,http 客户端只有一个,不同小程序配置可以实现多租户,所以似乎没必要单独再建新类存放?重复创建,增加了 http 客户端的成本?直接使用 Wx 实现类中已经有 configMap 不是更好吗?

## 解决方案

从 4.8.0 版本开始,我们为多租户 Spring Boot Starter 提供了**两种实现模式**供用户选择:

### 1. 隔离模式(ISOLATED,默认)

**实现方式**:为每个租户创建独立的 WxService 实例,每个实例拥有独立的 HTTP 客户端。

**优点**:
- ✅ 线程安全,无需担心并发问题
- ✅ 不依赖 ThreadLocal,适合异步/响应式编程
- ✅ 租户间完全隔离,互不影响

**缺点**:
- ❌ 每个租户创建独立的 HTTP 客户端,资源占用较多
- ❌ 适合租户数量不多的场景(建议 < 50 个租户)

**代码实现**:`WxMaMultiServicesImpl`, `WxMpMultiServicesImpl` 等

### 2. 共享模式(SHARED,新增)

**实现方式**:使用单个 WxService 实例管理所有租户配置,通过 ThreadLocal 切换租户,所有租户共享同一个 HTTP 客户端。

**优点**:
- ✅ 共享 HTTP 客户端,大幅节省资源
- ✅ 适合租户数量较多的场景(支持 100+ 租户)
- ✅ 内存占用更小

**缺点**:
- ❌ 依赖 ThreadLocal 切换配置,在异步场景需要特别注意
- ❌ 需要注意线程上下文传递

**代码实现**:`WxMaMultiServicesSharedImpl`, `WxMpMultiServicesSharedImpl` 等

## 使用方式

### 配置示例

```yaml
wx:
ma: # 或 mp, cp, channel
apps:
tenant1:
app-id: wxd898fcb01713c555
app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
tenant2:
app-id: wx1234567890abcdef
app-secret: 1234567890abcdef1234567890abcdef

config-storage:
type: memory
http-client-type: http_client
# 多租户模式配置(新增)
multi-tenant-mode: shared # isolated(默认)或 shared
```

### 代码使用(两种模式代码完全相同)

```java
@RestController
public class WxController {
@Autowired
private WxMaMultiServices wxMaMultiServices; // 或 WxMpMultiServices

@GetMapping("/api/{tenantId}")
public String handle(@PathVariable String tenantId) {
WxMaService wxService = wxMaMultiServices.getWxMaService(tenantId);
// 使用 wxService 调用微信 API
return wxService.getAccessToken();
}
}
```

## 性能对比

以 100 个租户为例:

| 指标 | 隔离模式 | 共享模式 |
|------|---------|---------|
| HTTP 客户端数量 | 100 个 | 1 个 |
| 内存占用(估算) | ~500MB | ~50MB |
| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 |
| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) |
| 适用场景 | 中小规模 | 大规模 |

## 支持的模块

目前已实现共享模式支持的模块:

- ✅ **小程序(MiniApp)**:`wx-java-miniapp-multi-spring-boot-starter`
- ✅ **公众号(MP)**:`wx-java-mp-multi-spring-boot-starter`

后续版本将支持:
- ⏳ 企业微信(CP)
- ⏳ 视频号(Channel)
- ⏳ 企业微信第三方应用(CP-TP)

## 迁移指南

### 从旧版本升级

升级到 4.8.0+ 后:

1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致)
2. **向后兼容**:所有现有代码无需修改
3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式

### 选择建议

**使用隔离模式(ISOLATED)的场景**:
- 租户数量较少(< 50 个)
- 使用异步编程、响应式编程
- 对线程安全有严格要求
- 对资源占用不敏感

**使用共享模式(SHARED)的场景**:
- 租户数量较多(> 50 个)
- 同步编程场景
- 对资源占用敏感
- 可以接受 ThreadLocal 的约束

## 注意事项

### 共享模式下的异步编程

如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递:

```java
// ❌ 错误:异步线程无法获取到正确的配置
CompletableFuture.runAsync(() -> {
wxService.getUserService().getUserInfo(...); // 可能使用错误的租户配置
});

// ✅ 正确:在主线程获取必要信息,传递给异步线程
String appId = wxService.getWxMaConfig().getAppid();
CompletableFuture.runAsync(() -> {
log.info("AppId: {}", appId); // 使用已获取的配置信息
});
```

## 详细文档

- 小程序模块详细说明:[spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md](spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md)

## 相关链接

- Issue: [#3835](https://github.com/binarywang/WxJava/issues/3835)
- Pull Request: [#3840](https://github.com/binarywang/WxJava/pull/3840)

## 致谢

感谢 issue 提出者对项目架构的深入思考和建议,这帮助我们提供了更灵活、更高效的多租户解决方案。
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# 微信小程序多租户配置说明

## 多租户模式对比

从 4.8.0 版本开始,wx-java-miniapp-multi-spring-boot-starter 支持两种多租户实现模式:

### 1. 隔离模式(ISOLATED,默认)

每个租户创建独立的 `WxMaService` 实例,各自拥有独立的 HTTP 客户端。

**优点:**
- 线程安全,无需担心并发问题
- 不依赖 ThreadLocal,适合异步/响应式编程
- 租户间完全隔离,互不影响

**缺点:**
- 每个租户创建独立的 HTTP 客户端,资源占用较多
- 适合租户数量不多的场景(建议 < 50 个租户)

**适用场景:**
- SaaS 应用,租户数量较少
- 异步编程、响应式编程场景
- 对线程安全有严格要求

### 2. 共享模式(SHARED)

使用单个 `WxMaService` 实例管理所有租户配置,所有租户共享同一个 HTTP 客户端。

**优点:**
- 共享 HTTP 客户端,大幅节省资源
- 适合租户数量较多的场景(支持 100+ 租户)
- 内存占用更小

**缺点:**
- 依赖 ThreadLocal 切换配置,在异步场景需要特别注意
- 需要注意线程上下文传递

**适用场景:**
- 租户数量较多(> 50 个)
- 同步编程场景
- 对资源占用有严格要求

## 配置方式

### 使用隔离模式(默认)

```yaml
wx:
ma:
# 多租户配置
apps:
tenant1:
app-id: wxd898fcb01713c555
app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
token: aBcDeFg123456
aes-key: abcdefgh123456abcdefgh123456abc
tenant2:
app-id: wx1234567890abcdef
app-secret: 1234567890abcdef1234567890abcdef
token: token123
aes-key: aeskey123aeskey123aeskey123aes

# 配置存储(可选)
config-storage:
type: memory # memory, jedis, redisson, redis_template
http-client-type: http_client # http_client, ok_http, jodd_http
# multi-tenant-mode: isolated # 默认值,可以不配置
```

### 使用共享模式

```yaml
wx:
ma:
# 多租户配置
apps:
tenant1:
app-id: wxd898fcb01713c555
app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad
tenant2:
app-id: wx1234567890abcdef
app-secret: 1234567890abcdef1234567890abcdef
# ... 可配置更多租户

# 配置存储
config-storage:
type: memory
http-client-type: http_client
multi-tenant-mode: shared # 启用共享模式
```

## 代码使用

两种模式下的代码使用方式**完全相同**:

```java
@RestController
@RequestMapping("/ma")
public class MiniAppController {

@Autowired
private WxMaMultiServices wxMaMultiServices;

@GetMapping("/userInfo/{tenantId}")
public String getUserInfo(@PathVariable String tenantId, @RequestParam String code) {
// 获取指定租户的 WxMaService
WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId);

try {
WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code);
return "OpenId: " + session.getOpenid();
} catch (WxErrorException e) {
return "错误: " + e.getMessage();
}
}
}
```

## 性能对比

以 100 个租户为例:

| 指标 | 隔离模式 | 共享模式 |
|------|---------|---------|
| HTTP 客户端数量 | 100 个 | 1 个 |
| 内存占用(估算) | ~500MB | ~50MB |
| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 |
| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) |
| 适用场景 | 中小规模 | 大规模 |

## 注意事项

### 共享模式下的异步编程

如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递:

```java
@Service
public class MiniAppService {

@Autowired
private WxMaMultiServices wxMaMultiServices;

public void asyncOperation(String tenantId) {
WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId);

// ❌ 错误:异步线程无法获取到正确的配置
CompletableFuture.runAsync(() -> {
// 这里 wxMaService.getWxMaConfig() 可能返回错误的配置
wxMaService.getUserService().getUserInfo(...);
});

// ✅ 正确:在主线程获取配置,传递给异步线程
WxMaConfig config = wxMaService.getWxMaConfig();
String appId = config.getAppid();
CompletableFuture.runAsync(() -> {
// 使用已获取的配置信息
log.info("AppId: {}", appId);
});
}
}
```

### 动态添加/删除租户

两种模式都支持运行时动态添加或删除租户配置。

## 迁移指南

如果您正在使用旧版本,升级到 4.8.0+ 后:

1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致)
2. **向后兼容**:所有现有代码无需修改
3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式

## 源码分析

issue讨论地址:[#3835](https://github.com/binarywang/WxJava/issues/3835)

### 为什么有两种设计?

1. **基础实现类的 `configMap`**:
- 位置:`BaseWxMaServiceImpl`
- 特点:单个 Service 实例 + 多个配置 + ThreadLocal 切换
- 设计目的:支持在一个应用中管理多个小程序账号

2. **Spring Boot Starter 的 `services` Map**:
- 位置:`WxMaMultiServicesImpl`
- 特点:多个 Service 实例 + 每个实例一个配置
- 设计目的:为 Spring Boot 提供更符合依赖注入风格的多租户支持

### 新版本改进

新版本通过配置项让用户自主选择实现方式:

```
用户 → WxMaMultiServices 接口
┌────┴────┐
↓ ↓
隔离模式 共享模式
(多Service) (单Service+configMap)
```

这样既保留了线程安全的优势(隔离模式),又提供了资源节省的选项(共享模式)。
Loading