背景
组件自治性的要求
云巧对组件的要求是可独立运行。 每个组件都是自包含的,在限界上下文中实现单个业务功能。 限界上下文是业务内的自然划分。
从技术层面,组件中的微服务就需要满足数据自包含的要求,即通过组件的代码版本管理实现数据的版本管理。
数据自包含的优点
数据自包含具有以下优点:
- 敏捷性:由于微服务是独立部署的,因此我们可以更轻松地管理 bug 修复和功能发布。 无需重新部署整个应用程序即可更新服务,出现问题时可回滚更新。 在很多传统应用程序中,如果在应用程序的一个部件中发现 bug,它会阻止整个发布流程。 新功能可能会被搁置,要等到 bug 修补程序后才能进行集成、测试和发布。
- 小型研发团队:微服务应该足够小,单个功能团队就能构建、测试和部署。 即使团队规模不大,也能大幅提升敏捷性。 而由于沟通效率更慢、管理开销更高且敏捷性更低,大型团队的工作效率往往更低。
- 小型代码库:在整体式应用程序,代码依赖项往往会随着时间的推移而变得混杂。 要在多个位置更改代码才能添加新功能。 如果不共享代码或数据存储,则微服务体系结构可将依赖项减到最少,使新功能的添加变得更容易。
- 数据隔离:执行架构更新要容易得多,因为只会影响单个微服务。 在整体应用程序中,更新架构可能极具挑战性,因为应用程序的不同部分可能都要获取相同的数据,对架构进行任何更改都会带来风险。
- 单元测试友好:通过代码库即可支持单元测试的数据用例管理。
挑战
数据自包含的优势并非没有代价。 下面是一些在使用数据自包含后可能会遇到的挑战:
- 管理规范:需要在研发团队内制定规范,保持统一的版本管理标准
- 数据完整性:通常建议采用最终一致性。
- 版本管理:多个服务可在任意给定时间更新,因此,若不精心设计可能会遇到向后或向前兼容性问题。
- 研发人员能力:评估团队是否具有所需的技能和经验。
敏捷交付迭代对分支和数据版本管理的要求
项目在推行敏捷交付,迭代周期不一定严格按照121模式。在2周的迭代周期内,分为4个小组。哪个小组完成了需求就先上线。
分支模式可以支持敏捷交付形式。
例如,在2020/12/1的时候定好了12/15是一个迭代周期,首先从master一个release/20201215。
在12/8的时候有一组已经完成需求1001可以发布了。那么就从master拉出一个release/20201208分支,将feature/1001合并到release/20201208分支,进行回归测试后发布release/20201208。发布完成后将release/20201208合并到master和release/20201215。
flyway的工作模式
Flyway是Spring官方推荐的数据自治和版本管理的工具。
Flyway支持对数据库进行升级,从任意一个版本升级到最新的版本。但是升级的依据是用户自己编写的sql脚本,用户自己决定每一个版本的升级内容。
Flyway不限定脚本里面的内容,但是对脚本文件的名称有一定的要求:
版本号可以使用小版本,如V1.1。
具体要求:
- 版本号和版本描述之间,使用两个下划线分隔。
- 版本描述之间,使用一个下划线分隔单词。
- 版本号唯一:不允许多个脚本文件有相同的版本号。
使用Flyway升级,flyway会自动创建一张历史记录表:flyway_schema_history。
这张表记录了每一次升级的记录,包括已经执行了哪些脚本,脚本的文件名,内容校验和,执行的时间和结果:
flyway在升级数据库的时候,会检查已经执行过的版本对应的脚本是否发生变化,包括脚本文件名,以及脚本内容。如果flyway检测到发生了变化,则抛出错误,并终止升级。
如果已经执行过的脚本没有发生变化,flyway会跳过这些脚本,依次执行后续版本的脚本,并在记录表中插入对应的升级记录。
所以,flyway总是幂等的,而且可以支持跨版本的升级。
交付项目中flyway的使用
项目中使用了flyway,所有环境的数据库脚本都通过flyway自动发布。
flyway的命名规范如下:
按照V+大版本号数字+.发布日期+.执行顺序(3位数字)+__操作(init/update/add/alter/modify)+_操作对象+_脚本创建人拼音.sql的格式
范例:V1.20211105.003__update_menu_wss.sql
范例中,flyway的版本号位V1.20211105.003。这个版本号必须是全局唯一的。
按照规范,交付项目中不允许直接操作任何环境的数据库,包括日常/测试/生产。所有的数据库变更都必须通过flyway来实施。这个方案的好处是确保了所有的上线脚本都能通过版本控制管理起来。
实践中遇到的问题
项目在敏捷交付过程中,对于flyway的使用遇到了两个问题:
- 同一个迭代的脚本里可能会存在顺序依赖。如果被依赖的脚本所在迭代晚于依赖的脚本所在迭代上线,那么上线的时候会发生错误
- 如果每个人都在自己的分支里提交脚本,可能版本号会有冲突,导致flyway执行失败。
- 考虑统一建立一个分支,所有的脚本只通过这个分支来提交
对于脚本顺序依赖,常见的情况为:脚本A中新增表/列/数据,脚本B依赖于脚本A中所建立的表/列/数据
解决思路
版本号冲突的问题比较好解。可以在每个迭代分支建立后,建立一个对应的分支。例如:feature/20201215_db。在项目组内约定这个分支只提交脚本。每位开发在提交完脚本后,将这个feature分支合并到自己的个人分支,以确保脚本最终也会合并到release分支上。
脚本顺序依赖的问题,需要项目组上贯彻3点:
- 平时功能测试只用日常环境发布,只在确定了上线前才用测试环境发布
- 每个脚本可重复执行
- 同一个迭代内的脚本之间禁止互相依赖
只在上线前才用测试环境,主要是用于验证上线脚本。这是第一重保险。
每个脚本可重复执行+同一个迭代内的脚本之间禁止互相依赖,这是第二重保险。
脚本可重复执行
要做到脚本可重复执行,可以参见这篇:https://blog.csdn.net/a2234150293/article/details/101546811
摘抄如下(有少量修正):
数据库脚本一般分为:
1.DDL(数据定义语言)操作对象是表,包含:Create、Drop、Alter
2.DML(数据操控语言)操作对象是数据,包含:Insert、Delete、Update
3.DCL(数据控制语言)操作对象是权限、用户,Grant、Revoke
DDL
Create
CREATETABLE IF NOT EXISTS `USER` ( `ID` int(11)NOTNULL AUTO_INCREMENT, `MOBILE` varchar(20) DEFAULT NULL COMMENT '手机', `NAME` varchar(20) DEFAULT NULL COMMENT '用户名', `PASSWORD` varchar(20) DEFAULT NULL COMMENT '密码', `IS_ENABLE` int(2) DEFAULT NULL COMMENT '是否有效 1有效 0无效', `CREATE_TIME` datetime DEFAULT NULL, `UPDATE_TIME` timestampNULL DEFAULT NULLONUPDATE CURRENT_TIMESTAMP, `CREATE_BY` varchar(20)NOTNULL DEFAULT 'admin' COMMENT '创建者,记录创建者信息', `LAST_UPDATE_BY` varchar(20)NOTNULL DEFAULT 'admin' COMMENT '修改者,记录修改者信息', PRIMARY KEY (`ID`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
Drop
DROPTABLE IF EXISTS `USER`;
Alter
CREATE PROCEDURE ADD_USER_ADDRESS()BEGIN IF NOT EXISTS (SELECT*FROM information_schema.COLUMNSWHERE table_schema ='wen'AND table_name ='USER'AND column_name ='ADDRESS') THEN ALTERTABLE `USER` ADD COLUMN `ADDRESS` varchar(20)NULL COMMENT '用户住址' AFTER `UPDATE_TIME`; END IF;END;CALL ADD_USER_ADDRESS;DROP PROCEDURE ADD_USER_ADDRESS;
DML
Insert
DELETEFROM USER WHERE ID =1;INSERTINTO `wen`.`USER` (`ID`, `MOBILE`, `NAME`, `PASSWORD`, `IS_DELETED`, `CREATE_TIME`, `LAST_UPDATE_TIME`, `CREATE_BY`, `LAST_UPDATE_BY`)VALUES('1','18888888888','测试用户','123456','2021-09-27 15:17:37',NULL,'admin','admin');
Delete
数据库一般不建议也不允许进行物理删除。
真的需要物理删除,参考下文update先进行备份。
Update
/*备份表与数据*/DROPTABLE IF EXISTS `USER_20210927`;/*第一次运行保留,后面删除*/CREATETABLE USER_20210927 LIKE USER;INSERTINTO USER_20210927 SELECT*FROM USER;/*软删除2021-09-23前数据*/UPDATE USER SET IS_ENABLE =0WHERE IS_ENABLE =1AND CREATE_TIME <'2021-09-27 00:00:00'