NCC 后台简析
参考
背景情况
部门开始推一个大项目 NCC,后台几乎沿用原 NC(有近十年历史),将原来 JavaSwing 画的重量前端换成了一种“轻量前端”,其实这种轻量前端一点都不轻量;SQL 完全是自己用字符串拼接起来的;开发模式是前后端分离联调的方式,但是因为后台代码都是部署在一个服务里头的,没有司库云(微服务架构)复杂。
从增删改查流程说起
对 Controller、Service 分层的讨论放在后面。
save
保存操作有以下特点。
- 批量
对普通的 CRUD 操作,最大的开销是网络传输,NCC 系统中操作的往往是多个表关联在一起的复杂结构,如果分成多次请求,一方面对用户体验不好,一方面对效率也有所影响。因此 NCC 中的做法是将同类数据放到同一页签,转换为 json 格式的数据,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42{
"pageid":"36650BIS_CARD",
"head":{
"head":{
"areaType":"form",
"rows":[
{
"values":{
"pk_bondissueregister":{
"display":null,
"value":null,
"scale":"-1"
}
...其他属性
},
"status":"2"
}
],
"areacode":"head"
}
},
"bodys":{
"repaymentplan":{
"rows":[
{
"rowid":"136270.438b34a6204411a",
"status":"2",
"values":{
"pk_bondrepaymentplan_b":{
"display":null,
"scale":null,
"value":""
}
...其他属性
}
}
],
"areaType":"table",
"areacode":"repaymentplan"
}
}
} - 差异化
新增和更新操作皆为调用 save 接口,通过区别传递的数据中是否有 id 来进行区分。另外,更新操作会先从数据库中查询出旧的数据,并和客户端传参进行比较,若完全一致则直接跳过此次更新。
delete
执行流程和 save 几乎一样,只是系统中不会真的删除一条数据,而是采用“逻辑删除”的方案。
query
查询操作的过滤条件是由前台给出的,我没有经历过重客户端时代 NC 的开发,但是在 Web 应用中也这么做有点奇怪。
page
分页查询的思路比较奇怪,是先按前端传递的过滤条件查出所有合适记录的 pk,然后在 Controller 层对这些 pk 进行分页:
1 | // ids指的是查出的所有合适记录的pk |
最后再调用一个批量查询的 Service 接口进行查询。
技术点
轻量前端、分层
轻量前端指的是用 React 开发的浏览器客户端,既然是轻量前端,那么做的事情肯定不会太多了,那么很多工作就必须要转移到后端了,因此在 NCC 中又引入了一个 Action 层,作为 Controller。
读写分离
后台 Service 层将读操作和写操作划分到了两个接口内,实际上应用层可以根据读写进行复制,一般来说读操作更频繁,可以多创建几个实例提供服务,并在前头使用负载均衡器进行反向代理。
SOA
SOA 是一种架构风格,与微服务的主要区别是 SOA 多了一个 ESB(企业服务总线),在 SOA 中,当一个服务需要调用另一个服务时,它需要先从 ESB 中获取那个服务的调用方法、消息格式等,而微服务可以看作一种轻量化的 SOA,它没有集中的一个 ESB,服务之间通过公开的 RPC 接口来相互调用。
实际上 NC 没有 ESB 这玩意。首先需要将 Service 接口及其对应的实现类注册到一个 xml 文件内,服务间调用通过 NCLocator.find(class)实现:如果配置文件中没有设置服务器 URL,则直接本地反射实例化 Service 的实现类;如果配置文件中设置了服务器 URL,则使用 JDK Proxy 做动态代理,将本地方法调用转换为远程调用。
元数据
元数据是描述数据的数据,实际开发业务前需要先使用相关工具”画”出元数据,NCC 的开发极大地依赖于元数据的正确性,老的功能节点的元数据若轻易修改,很有可能导致原功能出错。
模板
模板是对页面的抽象,平台的有关部门做了一个画模板的工具,由后端人员登录到工作桌面画模板,可以减轻前端开发人员的工作压力(可能是领导觉得前端比较稀缺?)。可以从元数据导出模板,并根据原型图进行适配。
随着业务的复杂化,将许多公共组件抽象出来集中管理是必须的,尤其是 NC 这么复杂的庞然大物,其业务拿出两个小时来也没办法讲完 1%,有点夸张,但是事实是这里的需求很多都是从会计等行业转来的,一次给我们讲需求时从 NC 上满屏的标签中挑了部分讲了两小时,大家都是崩溃的…因此 NC 中引入了三大模板的概念,它们分别是:查询模板、打印模板、计算模板,分别对查询 sql、打印、???进行了抽象。
依赖查找
Spring 的核心原理是 IoC,而 IoC 的主要实现方式是 DI 或依赖查找。DI 我们比较眼熟,主要有”属性注入”、”构造器注入”及”方法注入”。NCC 后台为 NC,已经有 10 来年历史,其中实现了一套依赖查找的模式,如下代码所示。
1 | IXxxService xxxService = ServiceLocator.find(IXxxService.class); |
当然接口和实现类之间不是自动关联在一起的,还需要定义一个 upm 文件来配置他们之间的关联关系。
这实际上和比较原始的 Spring 用法有点像,如下代码所示。
1 | XxxBean bean = application.getBean(XxxBean.class); |
内存锁
排他锁
就是普通的锁。
1 | boolean isOK=false; |
共享锁
共享锁适用于读者写者的情景,即某类操作可以并存、而和其他类操作不能的情况。
使用方法和排他锁差不多:
1 | PKLock.getInstance().acquireBatchLock( |
对同一 PK 来说,共享锁和排他锁是不能共存的。
动态锁
动态锁为一种特殊的锁,进行加锁的人员不用自己释放,其调用序列一般为:
pklock.acquireLock… pklock.addDynamicLock…
如果是远程调用,远程调用结束后系统会自动释放所有的动态锁
动态锁其实是对前面两种锁的封装,不过动态锁不需要主动解锁,中间件会在后台事务结束前进行统一解锁;并且在同一次中间件事务中,可以重复申请相同 PK 的锁。
1 | boolean isOK = PKLock.getInstance().addDynamicLock( PK ); |
动态锁的实现原理是事件机制,动态锁最后会发出一个解锁事件,业务操作执行结束后会触发这个事件。
时间戳校验
有了锁机制可以保证并发操作不会破坏数据一致性,但是不能保证用户的操作是对最新版本的数据进行的,因此需要引入时间戳机制。
主要通过比较当前对象的ts 值(时间戳)与数据库中该对象的 TS 值来判断用户修改的是否为最新版本的数据。在每次对数据进行修改操作时,都需要更新相应数据的时间戳标志 ts,这个一般由系统维护,不需要在业务中特殊处理。
动态代理
上面提到了动态锁机制但是没有具体说解锁的时机,假设所有含写操作的业务操作都需要加锁,难道要把加锁解锁逻辑都写一遍?NC 中为了解决这个问题也实现了一套 AOP 机制,原理是 JDK Proxy,NC 会为所有在配置文件中注册的 Service 创建动态代理,在动态代理中可以对锁等公共功能进行操作,从而大大简化业务代码。在这方面来讲,NC 也可以看作 Spring 的弱化版。
跨组件调用
NCC 后台是传统的单体式应用,所有模块几乎都放在一个服务里(用户买什么就把什么包含进来),跨模块的组件之间可能存在相互引用的关系,比如 a 需要将某些信息存入 b 中,就需要调用 b 提供的接口,而 b 在某些情况下也需要从 a 中获取某些信息,但 b 所在的 B 模块是一个更通用的组件,它不会为每个下游业务都新增代码。在 NCC 中,面对这种情况一般是由 a 来提供一个回调接口,将这个接口名保存到数据库中,然后告诉 b:”这就是 a 的接口啦”,类似于 RPC,不过将耦合的部分转移到了数据库中。
逻辑删除
数据库里的数据不会被直接删除,而是采取逻辑删除的策略,每条数据都有一个额外的dr(delete remark)字段用于标识一条数据是否被删除,查询时需要根据这个字段进行过滤。
Oracle
采用 Oracle 作为数据库,一是因为客户主要是大企业,安全、稳定有更高要求;二,数据量也确实不大,可能也用不着进行分库分表;三,作为延续了十几年的系统,当时“去 IOE 化”还没有提出,Oracle 仍是数据库领域最好的选择;最后,业务十分复杂,且底层 ORM 代码中存在大量直接使用字符串拼接出来的 SQL,想要完全移植到 MySQL 等数据库是不大现实的。
总结
以上我对最近工作中涉及到的东西进行了简单总结,嗯…产品底层仍有很多我未知的部分,一些与业务关联较大的代码也不适合拿出来分享,于是暂时告一段落吧。