在系统流量增长之后,数据库往往成为瓶颈,主从复制配合读写分离是常见的缓解手段。传统做法是在应用框架里配置多个数据源,由开发人员在每次调用时显式选择主库或者从库,这种做法侵入业务且容易遗漏。借助SQL视图,可以把读写目标的差异封装在数据库对象层面,让上层代码以统一方式访问,由视图定义来决定实际命中的是主库基表还是从库同步表。

一、读写分离与视图封装的基本思路
主从库读写分离的核心目标是:所有INSERT、UPDATE、DELETE等写操作进入主库,所有SELECT读操作尽量路由到从库,从而减轻主库负载并利用从库横向扩展查询能力。通常这个路由逻辑放在应用端,比如使用Spring的AbstractRoutingDataSource,或者MyBatis的插件拦截SQL类型。但这要求团队对每一个数据访问点都保持警惕。
SQL视图是建立在基表之上的虚拟表,它本身不存储数据,查询视图时数据库会执行视图定义中的SELECT语句。如果我们在从库上创建一个视图,其定义直接引用从库中已经通过复制同步过来的业务表,而在主库上让应用直接操作原表,那么应用只需要约定“读走视图、写走表”即可。这样,读写分离的逻辑被收敛到数据库 schema 设计里,而不是散落在代码仓库的各个Mapper中。
二、具体实现方案与代码示例
1. 主库与从库的表结构
假设有一个订单表 orders,主库与从库都有相同的结构。主库支持读写,从库通过 binlog 复制保持同步。我们在从库额外建立一个视图,用于给应用标识“这是只读入口”。
主库直接暴露表,从库建立视图的语句如下。注意在从库执行,视图名可以加前缀 rv_ 表示 read view,内部直接 select 基表,因为从库本身的数据就是主库同步过来的,不需要跨库查询。
-- 在从库执行 CREATE VIEW rv_orders AS SELECT id, user_id, amount, status, created_at FROM orders;
2. 应用层的访问约定
应用在执行写操作时,连接主库数据源,直接操作 orders 表;执行读操作时,连接从库数据源,查询 rv_orders 视图。由于视图字段与基表一致,ORM 映射无需改动,只需在 SQL 语句层面替换表名。
下面是一段伪代码展示如何在数据访问层做封装,而不让业务代码感知底层差异:
public class OrderRepository {
// 写操作使用主库数据源
public void insertOrder(Order o) {
String sql = "INSERT INTO orders(user_id, amount, status) VALUES (?, ?, ?)";
masterJdbcTemplate.update(sql, o.getUserId(), o.getAmount(), o.getStatus());
}
// 读操作使用从库数据源,访问视图
public Order findOrder(long id) {
String sql = "SELECT id, user_id, amount, status FROM rv_orders WHERE id = ?";
return slaveJdbcTemplate.queryForObject(sql, new OrderRowMapper(), id);
}
}
这种写法把“视图还是表”的选择固化在 Repository 内部。如果后续从库视图需要增加过滤条件,例如只暴露已支付订单,只需修改从库视图定义,不必动应用代码:
-- 调整从库视图,增加只读筛选 CREATE OR REPLACE VIEW rv_orders AS SELECT id, user_id, amount, status, created_at FROM orders WHERE status <> 'deleted';
三、查询分发的进阶封装
1. 利用同义词或代理视图统一名称
有些数据库支持同义词(synonym),可以在主库建一个叫 orders_read 的同义词指向自身表,在从库建同义词指向 rv_orders。这样应用无论连哪个库,都查 orders_read,由连接的数据源决定底层对象。不过 MySQL 不支持同义词,可以用代理库或中间件实现类似效果。
如果采用中间件如 ProxySQL,也可以把视图封装进一步上移:中间件识别 SELECT 自动转发从库,识别 DML 转发主库,此时应用甚至不用区分视图和表。但本文聚焦的是不引入额外中间件、仅用数据库视图完成逻辑封装的轻量方案。
2. 多从库场景下的视图分发
当存在多个从库时,可以为不同从库建立不同视图,比如 rv_orders_replica_a 和 rv_orders_replica_b,应用根据权重随机选择从库数据源,再访问对应视图。视图本身不带来性能损耗,它只是查询重写,真正影响效率的是从库负载和复制延迟。
| 方案 | 逻辑位置 | 维护成本 | 适用场景 |
|---|---|---|---|
| 代码层路由 | 应用框架 | 高,易遗漏 | 已用成熟ORM且团队规范强 |
| SQL视图封装 | 数据库schema | 低,改视图即可 | 希望减少业务侵入 |
| 中间件代理 | 独立层 | 中,需运维 | 多语言多服务共用 |
四、避坑点与注意事项
1. 复制延迟导致脏读
视图读从库无法避免主从复制延迟。如果用户刚下单马上查 rv_orders,可能查不到。需要在业务上区分“强一致读”和“弱一致读”,强一致走主库表,弱一致走从库视图。可以在 Repository 提供两个方法,而不是无脑走视图。
例如在订单支付成功页,必须查主库确认状态;在用户历史订单列表,可以查从库视图。这种粒度控制比全局读写分离更稳妥,也体现了视图封装的灵活性:它只是给你一个可选的读入口,而非强制所有读下沉。
2. 视图权限与账号隔离
从库账号应只授予视图的 SELECT 权限,禁止对基表直接操作,避免误写从库破坏复制。主库账号则限制来源 IP,仅允许应用服务器连接。通过权限体系配合视图,能减少人为故障。
另外,当主库表结构变更,如增加字段,从库视图若使用 SELECT * 会自动继承,但若显式列名则需手动改视图。推荐在从库视图中使用明确列名并随表变更同步更新,避免隐式依赖造成后期排查困难。
五、总结
通过SQL视图把主从库读写分离的逻辑封装起来,是一种低侵入、易维护的思路。它让读请求天然指向从库上的视图,写请求留在主库基表,应用层只需遵循简单约定。配合权限控制和延迟容忍设计,能够在没有复杂中间件的前提下,实现清晰的查询分发。对于中小团队或希望逐步演进架构的系统,这种基于视图的封装值得尝试。