设计模式与数据库:连接池中的享元、事务管理中的模板方法、ORM中的代理
数据库编程,说白了就是跟资源打交道。连接要省着用,事务要管好,对象关系映射要灵活。这三个场景,恰好对应了三个经典设计模式。我这些年做后端系统,几乎每天都在跟它们打交道。
今天咱们就聊聊:连接池里的享元模式、事务管理里的模板方法、ORM框架里的代理模式。这三个组合,是数据库编程的黄金搭档。
一、连接池中的享元模式
先说说连接池。你想想看,每次数据库操作都新建一个连接,那性能得多差?TCP三次握手、数据库认证,这些开销可不小。
享元模式的核心思想:共享细粒度对象,避免重复创建。连接池就是典型的享元池。
关键点:连接池中的连接对象是享元,它们被多个线程共享使用。但要注意,连接本身是有状态的(事务、隔离级别等),所以需要「借出-归还」机制。
我在项目中遇到过一个问题:连接池配置太小,高峰期大量请求等待连接,系统直接雪崩。后来我学乖了,连接池大小要压测确定,而且一定要设置等待超时。
连接池享元模式的核心结构
// 享元接口 - 数据库连接
public interface DbConnection {
void executeQuery(String sql);
void close();
}
// 具体享元 - 真实连接
public class RealConnection implements DbConnection {
private String connectionId;
private boolean inUse = false;
public RealConnection(String id) {
this.connectionId = id;
// 模拟建立真实连接
System.out.println("创建连接: " + id);
}
@Override
public void executeQuery(String sql) {
if (!inUse) {
throw new RuntimeException("连接未激活");
}
System.out.println(connectionId + " 执行: " + sql);
}
@Override
public void close() {
// 不是真正关闭,而是归还到池中
System.out.println(connectionId + " 归还到池");
}
public void markInUse() { this.inUse = true; }
public void markFree() { this.inUse = false; }
}
// 享元工厂 - 连接池
public class ConnectionPool {
private List<RealConnection> pool = new ArrayList<>();
private static final int MAX_POOL_SIZE = 10;
public ConnectionPool() {
// 预创建连接
for (int i = 0; i < 5; i++) {
pool.add(new RealConnection("conn-" + i));
}
}
public synchronized DbConnection getConnection() {
for (RealConnection conn : pool) {
if (!conn.isInUse()) {
conn.markInUse();
return conn;
}
}
// 池满,等待或新建(这里简化处理)
if (pool.size() < MAX_POOL_SIZE) {
RealConnection newConn = new RealConnection("conn-" + pool.size());
newConn.markInUse();
pool.add(newConn);
return newConn;
}
throw new RuntimeException("连接池已满");
}
public synchronized void releaseConnection(DbConnection conn) {
if (conn instanceof RealConnection) {
((RealConnection) conn).markFree();
}
}
}
避坑指南:我曾经在连接池里犯过一个低级错误——没有做连接有效性检测。数据库重启后,池里的连接全断了,但状态还是「可用」。结果所有请求都拿到死连接,系统瘫痪了半小时。后来我加上了心跳检测和自动重连。
享元模式在连接池中的变体
实际生产环境中的连接池(比如HikariCP、Druid)比上面复杂得多。它们通常包含:
- 空闲队列:存放空闲连接的容器
- 活跃队列:正在使用的连接
- 最小/最大连接数:控制资源使用
- 连接泄漏检测:自动回收长时间未归还的连接
嗯,这里要注意:享元模式要求享元对象的状态是「外部化」的。连接池里的连接,其「是否被使用」这个状态就是外部状态,由连接池管理,而不是连接自身。
二、事务管理中的模板方法模式
事务管理,说白了就是「开启事务 → 执行业务逻辑 → 提交或回滚」这个固定流程。你想想看,不管是什么业务,事务的骨架都是一样的,变化的只是中间的业务逻辑。
这不就是模板方法模式的典型场景吗?
模板方法模式的核心:在父类中定义算法的骨架,让子类实现具体步骤。事务管理正好符合这个结构。
事务模板的经典实现
// 抽象模板 - 事务管理器
public abstract class TransactionTemplate {
// 模板方法 - 定义事务骨架
public final Object execute(TransactionCallback callback) {
Object result = null;
try {
// 1. 开启事务
beginTransaction();
// 2. 执行业务逻辑(由子类或回调实现)
result = callback.doInTransaction();
// 3. 提交事务
commitTransaction();
return result;
} catch (Exception e) {
// 4. 异常时回滚
rollbackTransaction();
throw new RuntimeException("事务执行失败", e);
} finally {
// 5. 清理资源
cleanup();
}
}
// 抽象方法 - 由具体子类实现
protected abstract void beginTransaction();
protected abstract void commitTransaction();
protected abstract void rollbackTransaction();
protected abstract void cleanup();
}
// 回调接口 - 业务逻辑的抽象
public interface TransactionCallback {
Object doInTransaction();
}
// 具体模板 - JDBC事务管理器
public class JdbcTransactionTemplate extends TransactionTemplate {
private Connection connection;
public JdbcTransactionTemplate(Connection conn) {
this.connection = conn;
}
@Override
protected void beginTransaction() {
try {
connection.setAutoCommit(false);
System.out.println("JDBC事务已开启");
} catch (SQLException e) {
throw new RuntimeException("开启事务失败", e);
}
}
@Override
protected void commitTransaction() {
try {
connection.commit();
System.out.println("JDBC事务已提交");
} catch (SQLException e) {
throw new RuntimeException("提交事务失败", e);
}
}
@Override
protected void rollbackTransaction() {
try {
connection.rollback();
System.out.println("JDBC事务已回滚");
} catch (SQLException e) {
// 回滚失败通常只能记录日志
System.err.println("回滚事务失败: " + e.getMessage());
}
}
@Override
protected void cleanup() {
try {
connection.setAutoCommit(true);
connection.close();
} catch (SQLException e) {
// 忽略
}
}
}
// 使用示例
public class UserService {
private TransactionTemplate template;
public void transferMoney(String from, String to, double amount) {
template.execute(() -> {
// 业务逻辑 - 转账
System.out.println("从 " + from + " 转账 " + amount + " 到 " + to);
// 这里执行实际的SQL操作
return null;
});
}
}
个人经验:我习惯在模板方法里加入「重试机制」。比如遇到死锁异常,自动重试3次。这个逻辑放在模板里,所有事务都能受益,不用每个业务自己写重试代码。
模板方法在Spring事务中的体现
Spring的@Transactional注解底层就是模板方法模式。它的TransactionInterceptor相当于模板,PlatformTransactionManager是具体实现。你只需要写业务逻辑,事务的开启、提交、回滚都由框架搞定。
说白了,模板方法模式把「不变」和「变化」分开了。事务骨架不变,业务逻辑变化。这个思想在框架设计中无处不在。
三、ORM中的代理模式
ORM框架(比如MyBatis、Hibernate)里,代理模式用得特别多。最典型的就是延迟加载(Lazy Loading)。
你想想看,查询一个订单时,要不要同时加载它的所有商品详情?如果订单有100个商品,每个商品还有图片、评论...一次性加载完,性能就崩了。
代理模式的核心:为真实对象提供一个代理,控制对真实对象的访问。延迟加载就是典型的「按需加载」。
ORM延迟加载的代理实现
// 真实主题 - 商品详情
public class ProductDetail {
private String productId;
private String description;
private byte[] imageData; // 大字段,加载成本高
public ProductDetail(String id) {
this.productId = id;
// 模拟从数据库加载
loadFromDatabase();
}
private void loadFromDatabase() {
System.out.println("从数据库加载商品详情: " + productId);
// 实际数据库查询...
this.description = "商品描述...";
this.imageData = new byte[1024 * 1024]; // 1MB图片
}
public String getDescription() { return description; }
public byte[] getImageData() { return imageData; }
}
// 代理类 - 延迟加载代理
public class ProductDetailProxy extends ProductDetail {
private String productId;
private ProductDetail realDetail;
private boolean loaded = false;
public ProductDetailProxy(String id) {
super(id); // 注意:这里不会真正加载
this.productId = id;
// 代理初始化时不加载真实数据
System.out.println("创建代理对象,尚未加载数据");
}
@Override
public String getDescription() {
ensureLoaded();
return realDetail.getDescription();
}
@Override
public byte[] getImageData() {
ensureLoaded();
return realDetail.getImageData();
}
private void ensureLoaded() {
if (!loaded) {
// 第一次访问时真正加载
realDetail = new ProductDetail(productId);
loaded = true;
System.out.println("延迟加载完成");
}
}
}
// 使用示例
public class OrderService {
public void displayOrderSummary(String orderId) {
// 假设从ORM框架获取订单,商品详情是代理对象
ProductDetailProxy proxy = new ProductDetailProxy("prod-123");
// 只显示描述,不加载图片
System.out.println("商品描述: " + proxy.getDescription());
// 如果用户点击查看图片,才真正加载
// byte[] image = proxy.getImageData(); // 此时才加载
}
}
注意陷阱:我曾经在项目里遇到一个坑——在事务外访问延迟加载的属性,结果抛出了LazyInitializationException。这是因为Session已经关闭了,代理对象无法加载数据。解决方案要么是提前加载,要么是延长Session生命周期。
代理模式在ORM中的其他应用
除了延迟加载,代理模式在ORM中还有这些用途:
- 脏检查:代理对象记录属性变化,在提交时自动生成UPDATE语句
- 权限控制:代理检查当前用户是否有权访问某些字段
- 缓存:代理先查缓存,缓存未命中再查数据库
- 日志记录:代理自动记录SQL执行时间
说白了,代理模式就是在「调用方」和「真实对象」之间加了一层。这一层可以做很多事情,而且对调用方完全透明。
四、三种模式的协同关系
这三种模式在数据库编程中经常一起出现。我画了一张图来展示它们的关系:
从这张图可以看出:
- 连接池(享元)在最底层,提供可复用的数据库连接
- 事务管理(模板方法)在中间层,控制事务的生命周期
- ORM代理(代理模式)在最上层,为业务代码提供灵活的数据访问
这三层各司其职,又相互配合。连接池保证连接高效复用,事务管理保证数据一致性,ORM代理提供开发便利性。
我的建议:学习设计模式不要孤立地学。把它们放到真实场景中,看看它们怎么配合。比如你写一个数据库访问层,可以同时用到这三种模式。理解了它们的协同关系,你才能真正用好设计模式。
五、总结
今天聊了三个模式在数据库编程中的实战应用:
| 设计模式 | 数据库场景 | 核心作用 | 常见实现 |
|---|---|---|---|
| 享元模式 | 连接池 | 共享连接,避免重复创建 | HikariCP、Druid、C3P0 |
| 模板方法模式 | 事务管理 | 固定事务骨架,灵活业务逻辑 | Spring @Transactional、JTA |
| 代理模式 | ORM延迟加载 | 按需加载,透明增强 | MyBatis懒加载、Hibernate代理 |
说白了,设计模式不是空中楼阁。它们是从实际开发中提炼出来的最佳实践。你写代码时遇到重复问题,不妨想想:有没有现成的模式可以套用?
我个人习惯是:先理解问题本质,再选择模式。不要为了用模式而用模式。模式是工具,不是目的。
嗯,今天就聊到这里。这些模式你平时肯定都用过,只是可能没意识到它们就是设计模式。下次写数据库代码时,可以留意一下——你正在用哪个模式?