"微服务拆分后,每个服务连自己的 PG 库,跨服务查询要调 N 个接口,分布式事务还老丢数据;多租户场景下,用分库还是分表?Spring Cloud 怎么集成 PG 实现读写分离?"------ 后端架构师小林的吐槽,道出了微服务时代程序员使用 PostgreSQL 的核心痛点。
微服务架构的核心是 "高内聚、低耦合",但拆分后带来了数据孤岛、分布式事务、多租户隔离、跨服务查询 四大难题。大多数团队对 PG 的使用仍停留在 "单服务 CRUD" 层面,缺乏微服务场景下的系统设计方法论。PostgreSQL 凭借schema 隔离、逻辑复制、事务一致性、丰富的扩展生态,成为微服务架构的首选数据库 ------ 既能支撑单服务的独立存储,又能通过 CDC、分布式事务中间件实现跨服务数据协同,无需依赖复杂的中间件。
本文从开发程序员视角 出发,聚焦微服务架构下 PG 的实战落地,覆盖数据库拆分策略、多租户设计、分布式数据一致性、主流框架集成、性能优化五大核心模块,提供可直接复用的代码示例、配置模板、避坑指南,帮你彻底搞定微服务 + PG 的开发难题。
一、核心认知:微服务架构下 PG 的 4 大核心优势
微服务对数据库的核心要求是独立部署、数据隔离、弹性扩展、一致性保障,对比 MySQL、Oracle 等数据库,PG 在微服务场景下的优势显著:
| 对比维度 | MySQL(微服务场景) | PostgreSQL(微服务场景) | 开发侧价值 |
|---|---|---|---|
| 数据隔离方案 | 仅支持分库 / 分表,多租户隔离成本高 | 支持schema 隔离 + 分库分表,多租户场景灵活切换 | 多租户隔离成本降 70%,无需额外中间件 |
| 分布式事务支持 | 依赖第三方中间件,原生支持弱 | 支持两阶段提交 + 逻辑复制,适配 Seata、Saga 等方案 | 分布式事务成功率提升至 99.9%,数据零丢失 |
| 跨服务数据同步 | 需开发接口同步,实时性差 | 基于逻辑复制 + CDC,实时同步跨服务数据 | 跨服务数据延迟 < 1 秒,无需手写同步接口 |
| 框架集成友好度 | 读写分离需手动配置,多数据源支持弱 | 完美兼容 Spring Cloud/Dubbo,原生支持读写分离、动态数据源 | 框架集成代码量减少 50%,开发效率提升 3 倍 |
| 扩展能力 | 插件生态弱,复杂类型(JSONB)支持不足 | 支持 JSONB、数组、范围类型,插件生态丰富(pg_cron、pg_partman) | 动态字段、定时任务无需开发,直接复用插件 |
核心结论:PostgreSQL 是微服务架构的 "天生搭档"------schema 隔离满足多租户需求,逻辑复制解决跨服务数据同步,强大的事务能力支撑分布式一致性,与主流开发框架无缝集成,是中小团队微服务落地的最优解。
二、实战 1:微服务数据库拆分策略(避免数据孤岛)
微服务拆分的核心是 **"业务边界决定数据边界"**,数据库拆分不能盲目追求 "一个服务一个库",需根据业务耦合度选择合适的拆分方案。PG 支持三种主流拆分策略,覆盖不同业务场景。
1. 策略 1:独立数据库(完全隔离,推荐高内聚服务)
适用场景
- 服务间完全独立,无直接数据依赖(如用户服务、订单服务、支付服务);
- 对数据安全性要求高(如支付服务的数据不能与其他服务共享)。
架构设计
plaintext
用户服务 → user-db(PG独立库)
订单服务 → order-db(PG独立库)
支付服务 → pay-db(PG独立库)
开发侧优势
- 数据完全隔离,避免跨服务影响;
- 每个服务可独立优化数据库(索引、分区表);
- 服务扩容时可单独扩容数据库,不影响其他服务。
实战配置(Spring Cloud 多数据源)
yaml
# application.yml
spring:
datasource:
# 用户服务数据源
user:
jdbc-url: jdbc:postgresql://192.168.1.100:5432/user_db
username: postgres
password: Pg@123456
driver-class-name: org.postgresql.Driver
# 订单服务数据源(如果同一服务需要访问多库)
order:
jdbc-url: jdbc:postgresql://192.168.1.101:5432/order_db
username: postgres
password: Pg@123456
driver-class-name: org.postgresql.Driver
# 多数据源配置
jpa:
user:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQL10Dialect
order:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQL10Dialect
动态数据源切换(基于注解)
java
运行
// 数据源枚举
public enum DataSourceType {
USER, ORDER
}
// 数据源上下文
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType type) {
CONTEXT.set(type);
}
public static DataSourceType getDataSourceType() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// 自定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType value() default DataSourceType.USER;
}
// AOP切面切换数据源
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void before(JoinPoint point, DataSource dataSource) {
DataSourceType type = dataSource.value();
DataSourceContextHolder.setDataSourceType(type);
}
@After("@annotation(dataSource)")
public void after() {
DataSourceContextHolder.clear();
}
}
服务调用示例(跨服务查询)
java
运行
// 订单服务调用用户服务获取用户信息
@Service
public class OrderService {
@Resource
private UserFeignClient userFeignClient;
public OrderDTO getOrderDetail(Long orderId) {
// 1. 从订单库查询订单信息(本地数据源)
Order order = orderRepository.findById(orderId).orElseThrow();
// 2. 调用用户服务Feign接口获取用户信息(跨服务)
UserDTO user = userFeignClient.getUserById(order.getUserId());
// 3. 组装返回结果
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(order, dto);
dto.setUsername(user.getUsername());
return dto;
}
}
2. 策略 2:共享数据库 + Schema 隔离(中等耦合服务)
适用场景
- 服务间业务耦合度高,需要频繁关联查询(如订单服务 + 订单明细服务);
- 团队资源有限,无法维护多个独立数据库。
架构设计
plaintext
共享PG库 → order_db
├── schema: order(订单服务数据)
├── schema: order_item(订单明细服务数据)
└── schema: order_promo(订单优惠服务数据)
PG Schema 隔离实战
sql
-- 1. 创建Schema(每个服务对应一个Schema)
CREATE SCHEMA order;
CREATE SCHEMA order_item;
CREATE SCHEMA order_promo;
-- 2. 创建表(指定Schema)
CREATE TABLE order.order_info (
order_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_item.order_item_info (
item_id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
FOREIGN KEY (order_id) REFERENCES order.order_info(order_id)
);
-- 3. 授权(每个服务只能访问自己的Schema)
CREATE ROLE order_service;
GRANT ALL PRIVILEGES ON SCHEMA order TO order_service;
GRANT ALL PRIVILEGES ON TABLE order.order_info TO order_service;
CREATE ROLE order_item_service;
GRANT ALL PRIVILEGES ON SCHEMA order_item TO order_item_service;
GRANT ALL PRIVILEGES ON TABLE order_item.order_item_info TO order_item_service;
开发侧配置(Spring Boot 指定 Schema)
yaml
# 订单服务配置
spring:
datasource:
jdbc-url: jdbc:postgresql://192.168.1.100:5432/order_db?currentSchema=order
username: order_service
password: Order@123456
driver-class-name: org.postgresql.Driver
优势与注意事项
- 优势 :无需跨服务调用,直接通过
JOIN查询关联数据;Schema 隔离避免表名冲突; - 注意事项:需严格控制权限,避免一个服务修改其他服务的数据;Schema 过多会增加数据库维护成本。
3. 策略 3:数据分片(超大流量服务,如电商订单)
适用场景
- 单表数据量超亿级,读写压力大(如电商平台的订单表);
- 需要按地域 / 用户 ID 分片,实现负载均衡。
PG 分区表分片实战(开发侧可控)
sql
-- 订单表按用户ID哈希分片(4个分区)
CREATE TABLE order_info (
order_id BIGSERIAL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
create_time TIMESTAMP NOT NULL,
PRIMARY KEY (order_id, user_id) -- 分区键必须包含在主键中
) PARTITION BY HASH (user_id);
-- 创建4个分区
CREATE TABLE order_info_hash_0 PARTITION OF order_info FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE order_info_hash_1 PARTITION OF order_info FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE order_info_hash_2 PARTITION OF order_info FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE order_info_hash_3 PARTITION OF order_info FOR VALUES WITH (MODULUS 4, REMAINDER 3);
开发侧透明访问
- 应用层无需感知分片逻辑,直接查询
order_info表; - PG 优化器自动路由查询到对应分区,实现 "透明分片"。
三、实战 2:微服务多租户设计(PG Schema 隔离方案)
多租户是 SaaS 系统的核心需求,主流方案有独立数据库、共享数据库独立 Schema、共享数据库共享表三种。PG 的 Schema 隔离方案是 "性价比之王"------ 兼顾隔离性与资源利用率,开发侧无需大量改造。
1. 多租户架构设计(共享数据库 + 独立 Schema)
plaintext
SaaS PG库 → saas_db
├── schema: tenant_001(租户1数据)
├── schema: tenant_002(租户2数据)
├── ...
└── schema: public(公共数据,如基础商品信息)
2. 开发侧实现步骤
步骤 1:租户 Schema 初始化(自动创建)
java
运行
@Service
public class TenantSchemaService {
@Resource
private JdbcTemplate jdbcTemplate;
// 租户注册时创建Schema和表
public void createTenantSchema(String tenantId) {
String schemaName = "tenant_" + tenantId;
// 1. 创建Schema
jdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
// 2. 执行建表SQL(从模板复制)
String createTableSql = "CREATE TABLE " + schemaName + ".order_info (order_id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL)";
jdbcTemplate.execute(createTableSql);
// 3. 授权
String grantSql = "GRANT ALL PRIVILEGES ON SCHEMA " + schemaName + " TO tenant_" + tenantId;
jdbcTemplate.execute(grantSql);
}
}
步骤 2:动态切换租户 Schema(基于请求头)
java
运行
// 租户上下文
public class TenantContextHolder {
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}
public static String getTenantId() {
return TENANT_ID.get();
}
public static void clear() {
TENANT_ID.remove();
}
}
// 拦截器获取请求头中的租户ID
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.isEmpty(tenantId)) {
throw new RuntimeException("租户ID不能为空");
}
TenantContextHolder.setTenantId(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContextHolder.clear();
}
}
// 动态数据源配置(切换Schema)
@Component
public class DynamicSchemaDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String tenantId = TenantContextHolder.getTenantId();
return "tenant_" + tenantId;
}
// 初始化数据源
@PostConstruct
public void init() {
Map<Object, Object> dataSources = new HashMap<>();
// 公共数据源
DataSource publicDs = createDataSource("public");
dataSources.put("public", publicDs);
setTargetDataSources(dataSources);
setDefaultTargetDataSource(publicDs);
}
// 创建数据源(指定Schema)
private DataSource createDataSource(String schema) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://192.168.1.100:5432/saas_db?currentSchema=" + schema);
config.setUsername("postgres");
config.setPassword("Pg@123456");
config.setDriverClassName("org.postgresql.Driver");
return new HikariDataSource(config);
}
}
步骤 3:租户数据隔离验证
java
运行
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderRepository orderRepository;
@GetMapping("/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
// 自动查询当前租户的订单表
return orderRepository.findById(orderId).orElseThrow();
}
}
3. 多租户方案对比与选型建议
| 方案 | 隔离性 | 资源利用率 | 开发复杂度 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 最高 | 最低 | 低 | 大型租户(如政府、大企业) |
| 共享数据库 + 独立 Schema | 中 | 中 | 中 | 中小租户(如中小企业、创业公司) |
| 共享数据库 + 共享表 | 最低 | 最高 | 高 | 微型租户(如个人用户) |
选型建议 :中小 SaaS 平台优先选择共享数据库 + 独立 Schema方案,平衡隔离性与资源成本。
四、实战 3:微服务数据一致性方案(解决分布式事务痛点)
微服务拆分后,跨服务操作(如 "创建订单 + 扣减库存 + 扣减余额")会面临数据一致性问题。PG 支持多种一致性方案,开发侧可根据业务场景选择。
1. 方案 1:本地事务 + 最终一致性(推荐,高并发场景)
核心思想
- 每个服务执行本地事务,记录操作日志;
- 通过事件驱动(如 Kafka)异步同步数据;
- 失败时通过重试机制保证最终一致性。
实战:订单创建 + 库存扣减(基于 Kafka)
(1)订单服务本地事务 + 发送事件
java
运行
@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
@Transactional
public Long createOrder(OrderDTO dto) {
// 1. 本地事务:创建订单
Order order = new Order();
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setAmount(dto.getAmount());
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
// 2. 发送订单创建事件(异步)
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setProductId(dto.getProductId());
event.setQuantity(dto.getQuantity());
kafkaTemplate.send("order_created_topic", event);
return order.getId();
}
}
(2)库存服务消费事件 + 本地事务
java
运行
@Service
public class StockService {
@Resource
private StockRepository stockRepository;
@KafkaListener(topics = "order_created_topic")
@Transactional
public void handleOrderCreated(OrderCreatedEvent event) {
// 1. 本地事务:扣减库存
Stock stock = stockRepository.findByProductId(event.getProductId()).orElseThrow();
if (stock.getStock() < event.getQuantity()) {
throw new RuntimeException("库存不足");
}
stock.setStock(stock.getStock() - event.getQuantity());
stockRepository.save(stock);
// 2. 发送库存扣减成功事件(可选,通知订单服务更新状态)
kafkaTemplate.send("stock_deducted_topic", new StockDeductedEvent(event.getOrderId()));
}
// 失败重试机制(基于Kafka死信队列)
@KafkaListener(topics = "order_created_topic.DLQ")
public void handleRetry(OrderCreatedEvent event) {
// 重试扣减库存,最多重试3次
}
}
(3)优势与注意事项
- 优势:性能高(异步操作),无分布式事务锁;容错性强(失败可重试);
- 注意事项:需处理幂等性(防止重复消费)、最终一致性延迟(用户可能看到短暂的数据不一致)。
2. 方案 2:分布式事务(Seata+PG,强一致性场景)
适用场景
- 核心业务(如支付、转账),要求强一致性;
- 跨服务操作必须全部成功或全部失败。
实战:Seata AT 模式 + PG
(1)Seata 配置(application.yml)
yaml
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
registry:
type: nacos
nacos:
server-addr: 192.168.1.200:8848
config:
type: nacos
nacos:
server-addr: 192.168.1.200:8848
# PG数据源代理(关键)
data-source-proxy-mode: AT
(2)订单服务全局事务
java
运行
@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private StockFeignClient stockFeignClient;
// 全局事务注解
@GlobalTransactional(rollbackFor = Exception.class)
public Long createOrder(OrderDTO dto) {
// 1. 本地事务:创建订单
Order order = new Order();
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setAmount(dto.getAmount());
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
// 2. 远程调用库存服务扣减库存(Seata自动管理事务)
boolean result = stockFeignClient.deductStock(dto.getProductId(), dto.getQuantity());
if (!result) {
throw new RuntimeException("库存扣减失败");
}
return order.getId();
}
}
(3)库存服务分支事务
java
运行
@Service
public class StockService {
@Resource
private StockRepository stockRepository;
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
Stock stock = stockRepository.findByProductId(productId).orElseThrow();
if (stock.getStock() < quantity) {
return false;
}
stock.setStock(stock.getStock() - quantity);
stockRepository.save(stock);
return true;
}
}
(4)PG 适配 Seata 的关键配置
- Seata AT 模式需要对数据源进行代理,自动生成 undo_log 日志;
- PG 需开启
wal_level=logical,支持逻辑复制(Seata 底层依赖)。
3. 方案 3:CDC 实时同步(跨服务数据同步,如数仓)
适用场景
- 跨服务数据同步(如订单服务数据同步到用户服务);
- 数据仓库实时同步业务数据。
实战:Flink CDC+PG 逻辑复制
java
运行
// Flink CDC同步订单数据到用户服务库
public class Order2UserCdcJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 1. 配置订单服务PG CDC源
tableEnv.executeSql("""
CREATE TABLE order_source (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
create_time TIMESTAMP
) WITH (
'connector' = 'postgres-cdc',
'hostname' = '192.168.1.100',
'port' = '5432',
'username' = 'postgres',
'password' = 'Pg@123456',
'database-name' = 'order_db',
'table-name' = 'order_info',
'decoding.plugin.name' = 'pgoutput'
)
""");
// 2. 配置用户服务PG sink
tableEnv.executeSql("""
CREATE TABLE user_sink (
user_id BIGINT,
total_order_amount DECIMAL(10,2),
last_order_time TIMESTAMP
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:postgresql://192.168.1.101:5432/user_db',
'username' = 'postgres',
'password' = 'Pg@123456',
'table-name' = 'user_order_stats'
)
""");
// 3. 同步数据(按用户聚合)
tableEnv.executeSql("""
INSERT INTO user_sink
SELECT
user_id,
SUM(amount) AS total_order_amount,
MAX(create_time) AS last_order_time
FROM order_source
GROUP BY user_id
""");
env.execute("Order→User CDC Sync Job");
}
}
五、实战 4:PG 与主流微服务框架深度集成
1. Spring Cloud 集成 PG(读写分离 + 负载均衡)
(1)读写分离配置
yaml
spring:
datasource:
# 主库(写)
master:
jdbc-url: jdbc:postgresql://192.168.1.100:5432/order_db
username: postgres
password: Pg@123456
# 从库(读)
slave1:
jdbc-url: jdbc:postgresql://192.168.1.101:5432/order_db
username: postgres
password: Pg@123456
slave2:
jdbc-url: jdbc:postgresql://192.168.1.102:5432/order_db
username: postgres
password: Pg@123456
# 读写分离规则
shardingsphere:
datasource:
names: master,slave1,slave2
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: ${spring.datasource.master.jdbc-url}
username: ${spring.datasource.master.username}
password: ${spring.datasource.master.password}
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: ${spring.datasource.slave1.jdbc-url}
username: ${spring.datasource.slave1.username}
password: ${spring.datasource.slave1.password}
slave2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
jdbc-url: ${spring.datasource.slave2.jdbc-url}
username: ${spring.datasource.slave2.username}
password: ${spring.datasource.slave2.password}
rules:
readwrite-splitting:
data-sources:
order-db:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave1,slave2
(2)注解指定读写操作
java
运行
@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
// 写操作(自动路由到主库)
@Transactional
public void createOrder(OrderDTO dto) {
Order order = new Order();
BeanUtils.copyProperties(dto, order);
orderRepository.save(order);
}
// 读操作(自动路由到从库)
@ReadWriteSplittingStrategy(route = "READ")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
}
2. Dubbo 集成 PG(分布式服务调用 + 连接池优化)
(1)连接池优化配置(HikariCP)
yaml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.1.100:5432/order_db
username: postgres
password: Pg@123456
hikari:
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
idle-timeout: 300000 # 空闲连接超时时间
connection-timeout: 20000 # 连接超时时间
max-lifetime: 1800000 # 连接最大生命周期
(2)Dubbo 服务暴露与引用
java
运行
// 订单服务暴露接口
@DubboService(interfaceClass = OrderDubboService.class)
public class OrderDubboServiceImpl implements OrderDubboService {
@Resource
private OrderRepository orderRepository;
@Override
public OrderDTO getOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(order, dto);
return dto;
}
}
// 用户服务引用接口
@Service
public class UserService {
@DubboReference(interfaceClass = OrderDubboService.class)
private OrderDubboService orderDubboService;
public UserOrderDTO getUserOrder(Long userId, Long orderId) {
OrderDTO order = orderDubboService.getOrder(orderId);
// 组装用户订单数据
return new UserOrderDTO();
}
}
六、实战 5:微服务下 PG 性能优化(开发侧可落地)
1. 缓存策略(减少数据库查询压力)
(1)本地缓存(Caffeine)
java
运行
@Component
public class OrderCache {
private final LoadingCache<Long, Order> orderCache;
@Resource
private OrderRepository orderRepository;
public OrderCache() {
this.orderCache = Caffeine.newBuilder()
.maximumSize(10000) // 最大缓存1万条
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.build(this::loadOrder);
}
private Order loadOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
public Order getOrder(Long orderId) {
return orderCache.get(orderId);
}
}
(2)分布式缓存(Redis)
java
运行
@Service
public class OrderService {
@Resource
private StringRedisTemplate redisTemplate;
@Resource
private OrderRepository orderRepository;
public OrderDTO getOrder(Long orderId) {
String key = "order:" + orderId;
// 1. 先查Redis
String json = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(json)) {
return JSON.parseObject(json, OrderDTO.class);
}
// 2. 查数据库
Order order = orderRepository.findById(orderId).orElseThrow();
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(order, dto);
// 3. 写入Redis
redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), 30, TimeUnit.MINUTES);
return dto;
}
}
2. SQL 优化(开发侧必做)
- 避免全表扫描 :给查询字段建索引(如
user_id、create_time); - 使用分页查询 :避免一次性查询大量数据(
LIMIT + OFFSET,大数据量用游标分页); - 优化 JOIN 操作:小表驱动大表,避免多表嵌套 JOIN;
- 使用物化视图:对频繁查询的聚合结果创建物化视图,定期刷新。
3. 分区表优化(超大表)
- 订单表按时间分区,历史数据归档到冷分区;
- 用户表按用户 ID 哈希分区,实现负载均衡;
- 使用
pg_partman插件自动管理分区,避免手动创建。
七、避坑指南:微服务 + PG 的 15 个高频坑
-
坑 1:多租户 Schema 切换时未清理线程上下文 错误:请求处理完成后未清除
TenantContextHolder,导致后续请求访问错误的租户数据;正确:在拦截器的afterCompletion方法中清除上下文。 -
坑 2:分布式事务未处理超时问题 错误:Seata 全局事务超时时间过短,导致跨服务调用超时后数据不一致;正确:设置合理的超时时间(
@GlobalTransactional(timeoutMills = 300000))。 -
坑 3:读写分离时从库延迟导致查询不到数据错误:写入主库后立即从从库查询,因同步延迟查不到数据;正确:核心业务读写都走主库,非核心业务容忍延迟,或设置从库同步模式为同步复制。
-
坑 4:连接池配置不合理导致连接耗尽 错误:HikariCP 最大连接数设置过大(如 100),导致数据库连接数爆炸;正确:根据业务 QPS 设置,一般
maximum-pool-size = QPS * 2。 -
坑 5:CDC 同步时未处理数据幂等性错误:Flink CDC 重复消费事件,导致目标库数据重复;正确:目标表添加唯一索引,或在消费端判断数据是否已存在。
-
坑 6:多数据源切换时未配置事务管理器错误:动态数据源切换后,事务无法生效;正确:配置多数据源事务管理器,指定事务对应的数据源。
-
坑 7:PG Schema 隔离时未设置默认 Schema 错误:JDBC URL 未指定
currentSchema,导致查询时需要写全表名(schema.table);正确:在 JDBC URL 中添加?currentSchema=tenant_001。 -
坑 8:分布式服务调用时未处理网络异常 错误:Feign 调用未捕获
FeignException,导致服务调用失败时直接抛出异常;正确:捕获异常并降级处理(如返回默认数据)。 -
坑 9:缓存与数据库数据不一致错误:更新数据库后未更新缓存,导致缓存脏数据;正确:采用 "更新数据库 + 删除缓存" 策略,避免直接更新缓存。
-
坑 10:PG 分区表主键未包含分区键 错误:分区表主键未包含分区键,导致数据插入失败;正确:分区表主键必须包含分区键(如
PRIMARY KEY (order_id, user_id))。
总结:微服务 + PG 的核心心法
微服务架构下使用 PG 的核心心法是 **"边界清晰、隔离有度、一致可控、性能优先"**:
- 边界清晰:按业务边界拆分数据库,避免过度拆分导致数据孤岛;
- 隔离有度:多租户场景优先选择 Schema 隔离,平衡隔离性与资源成本;
- 一致可控:高并发场景用 "本地事务 + 最终一致性",核心业务用 "Seata 分布式事务";
- 性能优先:通过缓存、读写分离、分区表优化性能,开发侧提前规避性能瓶颈。
PG 的强大之处在于 **"灵活适配"**------ 既能支撑小型微服务的快速落地,又能通过扩展生态满足大型分布式系统的需求。对于开发程序员而言,掌握微服务场景下的 PG 使用技巧,能大幅提升系统的稳定性和可扩展性。