{"id":18050186,"url":"https://github.com/drtrang/dynamic-data-source-demo","last_synced_at":"2025-04-10T12:41:15.265Z","repository":{"id":87814076,"uuid":"88827318","full_name":"drtrang/dynamic-data-source-demo","owner":"drtrang","description":"基于事务的读写分离","archived":false,"fork":false,"pushed_at":"2018-02-07T03:05:18.000Z","size":143,"stargazers_count":45,"open_issues_count":0,"forks_count":20,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-03-24T11:21:34.540Z","etag":null,"topics":["database","dynamic","spring","spring-boot","transaction"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/drtrang.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-04-20T06:07:37.000Z","updated_at":"2024-06-28T09:31:47.000Z","dependencies_parsed_at":null,"dependency_job_id":"4dfc9e3d-4d13-44bc-a4b4-ae633b1e0b79","html_url":"https://github.com/drtrang/dynamic-data-source-demo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drtrang%2Fdynamic-data-source-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drtrang%2Fdynamic-data-source-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drtrang%2Fdynamic-data-source-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drtrang%2Fdynamic-data-source-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/drtrang","download_url":"https://codeload.github.com/drtrang/dynamic-data-source-demo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248217156,"owners_count":21066634,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","dynamic","spring","spring-boot","transaction"],"created_at":"2024-10-30T21:10:52.227Z","updated_at":"2025-04-10T12:41:15.258Z","avatar_url":"https://github.com/drtrang.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 应用层读写分离的改进\n\n\n## 背景\n\n数据库读写分离是构建高性能 Web 架构不可缺少的一环，其主要提升在于：\n\n1. 主从职责单一，主写从读，可以极大程度地缓解 X 锁和 S 锁的竞争，并且可以进行针对性调优\n2. 请求分流，减少主库压力\n3. 当读成为 DB 瓶颈时，很容易进行水平拓展\n4. 增加冗余，实现高可用，出现故障后可快速恢复，仅丢失少量数据或不丢失数据\n\n\n## 实现方式\n\n读写分离首先需要 DB 实例的支持，配置主库、从库以及主从同步策略，此步骤一般交给 OP 即可。实例搭建完毕后，我们就可以开发相应模块，以实现真正的读写分离。\n\n业界的实现方式一般分为两种：**DB 中间件** 和 **应用层读写分离**，二者均有各自的优缺点，详情见下表：\n\n### DB 中间件\n\n\u003e 优点：对于应用透明；不限语言\u003cbr\u003e\n\u003e 缺点：专人部署 + 维护；保证 HA、LB；一般只支持 MySQL\n\n### 应用层读写分离\n\n\u003e 优点：开发简单，团队内部可以自行消化；基于 JDBC 驱动或框架，理论支持任意类型的 DB\u003cbr\u003e\n\u003e 缺点：通用性差，各应用需要自己实现；手动指定数据源\n\n用不用 DB 中间件需要考虑实际情况，如数据体量和有没有人维护等等，本文讲的是应用层读写分离。\n\n## 当前方案\n\n通过自定义注解 `@DataSourceRoute`，手动声明当前方法操作的数据源，再通过切面拦截该切入点，路由到目标数据源。\n\n因为实际中还要与事务结合，所以又写了一套基于事务路由主从数据源的切面，使用起来较为繁琐。\n\n```java\n//annotation\npublic @interface DataSourceRoute {\n    AccessType type() default AccessType.MASTER;\n}\n\n//aspect\npublic class DataSourceRouteAspect {\n    @Before(\"@annotation(DataSourceRoute)\")\n    public void before(JoinPoint point) {\n        Method targetMethod = ((MethodSignature) point.getSignature()).getMethod();\n        DataSourceRoute annotation = targetMethod.getAnnotation(DataSourceRoute.class);\n        DynamicDataSourceHolder.route(annotation.type());\n    }\n}\n\n//dao\n@DataSourceRoute(type=AccessType.SLAVE)\npublic Housedel findByPK(Long housedelCode) {\n    return mapper.findByPK(housedelCode);\n}\n```\n\n\n## 改进方案\n\n其实总结一下我们使用读写分离的场景会发现，主库一般负责写入（偶尔用来读），从库则全部用来读取。而为了保障数据的正确性，我们在写入操作时一般会加上事务（这也是我推荐的最佳实践），也就是说，大部分事务操作是在写入，大部分非事务操作则是在读取，由此可见读写分离和事务之间是有一定关联的。\n\n既然思路是可行的，那我们不妨思考一下，实际使用中具体有哪些场景呢？\n\n| 序号 | 事务 | 数据源 | 操作\n| :-- | :-- | :-- | :-- |\n| 1 | 无 | 从库 | 读\n| 2 | 无 | 主库 | 读\n| 3 | 有 | 从库 | 读\n| 4 | 有 | 主库 | 写\n\n第 1 种，无事务从库读取。典型的只读场景，我们的业务场景一般是读多写少，为了方便，可以作为默认选项。\n\n第 2 种，无事务主库读取。主库中读取数据的情况还是比较少见的，一般是因为对数据的实时性要求较高，而 MySQL 的主从复制是异步的，中间会有短暂的时间差，为了保证数据的一致性，会直接从主库读取。\n\n第 3 种，有事务从库读取。前面我们说道，事务一般加在写入操作上，但也有个别情况只读时也需要加入事务，比如在当前只读事务内，不希望其它事务更改数据，从而保证数据前后的一致性。\n\n第 4 种，有事务主库写入。典型的写入场景，数据写入主库后，异步复制到从库。\n\n\n## 落地\n\n那么如何实现呢？阅读 Spring 的源码会发现，`DataSourceTransactionManager` 是 Spring 用来管理事务的类，我们只需要自定义一个事务管理器，在开启事务之前指定数据源即可。\n\n有了之前的分析，我们可以得到以下规则：默认无事务时路由到从库，**有事务且非只读**时路由到主库。\n\n1. 定义动态数据源\n```java\npublic class DynamicDataSource extends AbstractRoutingDataSource {\n    public DynamicDataSource(Object defaultTargetDataSource, Map\u003cObject, Object\u003e targetDataSources) {\n        super.setDefaultTargetDataSource(defaultTargetDataSource);\n        super.setTargetDataSources(targetDataSources);\n    }\n    \n    @Override\n    protected Object determineCurrentLookupKey() {\n        return DynamicDataSourceHolder.get();\n    }\n}\n\npublic final class DynamicDataSourceHolder {\n    public static final String MASTER_DATA_SOURCE = \"Master\";\n    public static final String SLAVE_DATA_SOURCE = \"Slave\";\n\n    private static final ThreadLocal\u003cString\u003e CONTAINER = ThreadLocal.withInitial(\n            () -\u003e DynamicDataSourceHolder.SLAVE_DATA_SOURCE\n    );\n\n    public static void routeMaster() {\n        CONTAINER.set(MASTER_DATA_SOURCE);\n    }\n    public static void routeSlave() {\n        CONTAINER.set(SLAVE_DATA_SOURCE);\n    }\n    public static String get() {\n        return CONTAINER.get();\n    }\n    public static void clear() {\n        CONTAINER.remove();\n    }\n}\n```\n\n2. 声明动态数据源\n```java\n@Configuration\npublic class SpringDataSourceConfig {\n\n    @Bean(initMethod = \"init\", destroyMethod = \"close\")\n    public DruidDataSource masterDataSource() {\n        return new DruidDataSource();\n    }\n\n    @Bean(initMethod = \"init\", destroyMethod = \"close\")\n    public DruidDataSource slaveDataSource() {\n        return new DruidDataSource();\n    }\n\n    @Bean\n    @Primary\n    public DynamicDataSource dataSource(DruidDataSource masterDataSource, DruidDataSource slaveDataSource) {\n        Map\u003cObject, Object\u003e targetDataSources = ImmutableMap.builder()\n                .put(MASTER_DATA_SOURCE, masterDataSource)\n                .put(SLAVE_DATA_SOURCE, slaveDataSource)\n                .build();\n        return new DynamicDataSource(slaveDataSource, targetDataSources);\n    }\n}\n```\n\n3. 重写 Spring 默认的事务管理器\n```java\npublic class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {\n\n    public DynamicDataSourceTransactionManager(DataSource dataSource) {\n        super(dataSource);\n    }\n    \n    @Override\n    protected void doBegin(Object transaction, TransactionDefinition definition) {\n        if (!definition.isReadOnly()) {\n            DynamicDataSourceHolder.routeMaster();\n        }\n        super.doBegin(transaction, definition);\n    }\n\n    @Override\n    protected void doCleanupAfterCompletion(Object transaction) {\n        DynamicDataSourceHolder.clear();\n        super.doCleanupAfterCompletion(transaction);\n    }\n}\n```\n\n4. 声明自定义的事务管理器\n```java\n@Configuration\n@EnableTransactionManagement(proxyTargetClass = true)\npublic class SpringDaoConfig implements TransactionManagementConfigurer {\n    @Autowired\n    private DynamicDataSource dataSource;\n\n    @Override\n    @Bean\n    public PlatformTransactionManager annotationDrivenTransactionManager() {\n        return new DynamicDataSourceTransactionManager(dataSource);\n    }\n}\n```\n\n代码贴完了，让我们来看看能不能满足之前的 4 种场景呢？\n\n其中 1、4 的区别仅仅是加不加事务，比较简单，那么待解决的还有 2 和 3。第 2 种因为没有事务，需要我们手动指定数据源，第 3 种则使用 Spring 提供的只读事务即可实现。\n\n| 序号 | 事务 | 数据源 | 操作 | 实现方式\n| :-- | :-- | :-- | :-- | :-- |\n| 1 | 无 | 从库 | 读 | 默认\n| 2 | 无 | 主库 | 读 | 手动指定 `DynamicDataSourceHolder.routeMaster()`\n| 3 | 有 | 从库 | 读 | `@Transactional(readOnly = true)`\n| 4 | 有 | 主库 | 写 | `@Transactional`\n\n如此一来，之前的问题都已经解决。我们仅仅通过 Spring 自带的 `@Transactional` 注解即可指定数据源，对比之前简化不少。\n\n\n## 硬广\n\n由于篇幅原因，文章中没有展示具体的执行结果。完整代码已打包成 `dynamic-data-source-demo`项目，并上传到 [Github](https://github.com/drtrang/dynamic-data-source-demo)，项目中提供完整的单元测试，详情大家可以 Clone 到本地自己执行一遍。\n\n`dynamic-data-source-demo` 项目基于 Spring Boot，集成了 MyBatis、通用 Mapper、PageHelper、Druid、Copiers，可以作为简单的脚手架使用，欢迎大家 Star 或者 Fork 到自己的仓库。\n\n如果有问题，可以在 Github 上提 Issue，或者 QQ 交流，以下是联系方式：\u003cbr\u003e\n我的 Github 地址：https://github.com/drtrang\u003cbr\u003e\n项目 Github 地址：https://github.com/drtrang/dynamic-data-source-demo\u003cbr\u003e\nBeanCopier 工具：https://github.com/drtrang/Copiers\u003cbr\u003e\nQQ：349096849\n\n\n[^1]: 只读事务：http://www.blogjava.net/terry-zj/archive/2005/12/06/22792.html\n\n[^2]: Spring的动态数据源：http://www.cnblogs.com/surge/p/3582248.html\n\n[^3]: Spring事务源码分析：http://www.cnblogs.com/lcxdever/p/4570090.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrtrang%2Fdynamic-data-source-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdrtrang%2Fdynamic-data-source-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrtrang%2Fdynamic-data-source-demo/lists"}