程序员shengfq

技术博客


  • Home

  • Archives

查控预警规则的链式调度

Posted on 2023-03-13

今日心得:

做别人不能做的事情,不敢做的事情,有难度的事情,本身就是一种自信,不管结果怎么样,不要怕,也不要觉得别人不会的我也不会,就是因为别人不会,我才有机会挑战不可能,才能体现我的价值.

需求场景

资金查控是一个应用工具软件,实现从执法办调取案件数据和卡号信息,在本系统内发起申请单,向公安部API发起查控申请,通过异步异地文件交互的方式获取结果.系统是给基层民警办案提供的协查工具,经过上级单位审批提交给公安部统一协调,由银行执行查控结果返回.

执法监督-查控预警模块设计,是民警基于线上系统长时间运行后,对业务运维上提出的新思考,在业务进行时,存在一些不合法的使用场景,于是民警提出了一个excel表格来记录业务违反规则的场景,希望我们系统能提供自动判断违规使用的查控申请,并将案件和账号及违规内容记录到预警内容,提供给省厅民警进行核查.这个规则列表是根据业务进行后会补充的,也会有调整的.甚至会启停.

问题清单 是否可通过现有资金查控平台监督 现有资金查控平台工作要求或无法通过平台监督原因 资金查控平台执法监督功能改造措施 执法监督设计逻辑 (粗体要求系统开发)
规则1 xxx xxxxx ccccc

思考

当时第一次接到这个需求的时候,我没有多想,就是在业务代码中增加拦截规则匹配,基本实现思路就是流水式代码.当我实现了其中几个规则之后,我发现代码越来越难维护,因为每个规则流程都要经历查询待办的案件数据,对案件中的几个关键指标根据规则的业务逻辑进行匹配,如果匹配上就增加案件的预警记录,然后标记该案件在在该规则上的执行任务状态,我要记录很多个执行状态,代码越来越难维护.

经过反复的思考业务场景的特点, 数据源是案件编号—>获取案件编号—>调用规则匹配—->标记规则完成.其他的规则控制逻辑相同,规则逻辑不同.而且这些规则都需要匹配完成, 这就是一个典型的责任链模式, 责任处理器就是规则处理,这个处理器抽象有两个方法,一个是处理匹配规则,一个是设置下一个处理器.然后我从定时调度任务开启一个任务线程获取待处理的案件列表,经过规则处理器之后,将结果写入到案件列表状态.

设计代码如下:

1
2
3
4
5
6
7
8
IHandler.java //链式规则处理接口
RequestHandler.java//链式规则处理抽象类
ChainFactoryBuilder.java//组链,初始化链路bean,执行链式调用
FlwsValidateHandler.java//业务规则实例
Request.java //请求对象 包含案件对象
RequestType.java //规则枚举
IScheduleExecutor.java //任务接口 获取数据/执行任务/结束任务
ScheduleExecutorTask.java //任务接口实现
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
/**

\* ClassName: ChainFactoryBuilder

\* Description:组链,初始化链路bean,执行链式调用

*

\* @author shengfq

\* @date: 2023/3/11 11:08 上午

*/

@Service

public class ChainFactoryBuilder {

private IHandler chain;

@Autowired

FlwsValidateHandler flwsValidateHandler;

@Autowired

public ChainFactoryBuilder(){

​ buildChain();

}

/**

\* TODO 构建责任链处理器链路

\* 如果有新的规则添加,则设置

\* handler.setNext(next);

\* 目前只有FlwsValidateHandler

\* */

@PostConstruct

private void buildChain() {

​ IHandler head=flwsValidateHandler;

​ //TODO 构建责任链处理器链路,新增的责任处理器要在此处上链

​ // head.setNext(iHandler);

​ chain=head;

}

/**

\* 压栈处理

\* */

public void makeRequest(List<Request> requests) {

​ if(CollectionUtil.isNotEmpty(requests)){

​ requests.stream().forEach(item->{

​ chain.handleRequest(item);

​ });

​ }

}



}



public interface IHandler {

/**

\* 处理请求

\* */

void handleRequest(Request req);

/**

\* 设置下一个处理器

\* */

void setNext(IHandler next);

}



/**

\* 责任链处理器抽象类

\* 定义公共执行方法的实现

\* @author sheng

\* @date 2023-03-11

\* */

public abstract class RequestHandler implements IHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(RequestHandler.class);
private IHandler next;
@Override

public void setNext(IHandler next) {

this.next = next;

}


/**

\* Request handler

*/

@Override

public void handleRequest(Request req) {

if (next != null && !req.isHandled()) {

next.handleRequest(req);

}

}



protected void printHandling(Request req) {

LOGGER.info("{} 处理规则 \"{}\"", this, req);

}

@Override

public abstract String toString();

}

/**

*请求对象 包含案件对象

*/

@Data

public class Request {



private final RequestType requestType;



private final YjAjxxtjInfo yjAjxxtjInfo;



private boolean handled;



public Request(final RequestType requestType, final YjAjxxtjInfo yjAjxxtjInfo) {

this.requestType = Objects.requireNonNull(requestType);

this.yjAjxxtjInfo = Objects.requireNonNull(yjAjxxtjInfo);

}



public RequestType getRequestType() {

return requestType;

}



public void markHandled() {

this.handled = true;

}



public boolean isHandled() {

return this.handled;

}





@Override

public String toString() {

return requestType.getDesc();

}

}





/**

*枚举规则匹配

\* @author shengfq

\* @date 2023-03-09

*/

@Getter

public enum RequestType {

WLADJZH,

WCXYEDJZH,

DJFFZSAZH,

DJJECGSAJE,

CFDJZH,

CADJ,

WABFSSXZ,

WYFJSJDZH,

CXWGZH,

FLWSNRQS;

}



/**

\* 定时任务执行器接口

\* */

public interface IScheduleExecutor {

/**

\* 获取待查的案件信息

\* */

List<Request> begin(YjAjxxtjInfo ajxxtjInfo);

/**

\* 责任链处理规则

\* */

void handler(List<Request> requests);

/**

\* 将结果更新到统计记录表

\* yj_ajxxtj_info

\* */

int end(YjAjxxtjInfo request);

}



/**

\* 定时任务类

\* */

@Slf4j

@Service

public class ScheduleExecutorTask implements IScheduleExecutor {

@Autowired

YjAjxxtjInfoService yjAjxxtjInfoService;

@Autowired

ChainFactoryBuilder chainFactoryBuilder;

/**

*TODO 给目标案件添加预警规则匹配

*/

@Override

public List<Request> begin(YjAjxxtjInfo ajxxtjInfo) {

​ log.info("获取待处理的案件信息 ajxxtjInfo{} ",ajxxtjInfo);

​ List<Request> requests=new ArrayList<>();

​ //法律文书校验规则处理

​ Request request=new Request(RequestType.FLWSNRQS,ajxxtjInfo);

​ //TODO 有后续规则通过 新增 Request对象规则加入规则队列

​ requests.add(request);

​ return requests;

}

/**

\* 责任链处理规则

\* */

@Override

public void handler(List<Request> requests){

​ log.info("开始执行规则匹配");

​ chainFactoryBuilder.makeRequest(requests);

}



/**

\* 将结果更新到统计记录表

\* yj_ajxxtj_info

*/

@Override

public int end(YjAjxxtjInfo ajxxtjInfo) {

​ log.info("完成预警规则匹配");

​ int result=0;

​ if(ajxxtjInfo!=null){

​ YjAjxxtjInfo update=new YjAjxxtjInfo();

​ update.setAjbh(ajxxtjInfo.getAjbh());

​ update.setSfyj(QueryParamEnum.SFYJ_YES);

​ result=yjAjxxtjInfoService.updateByAjbh(update);

​ }

​ return result;

}

}



/**

*

\* FlwsValidateHandler

*法律文书内容校验处理器

*/

@Slf4j

@Service

public class FlwsValidateHandler extends RequestHandler{

@Autowired

ZjckFlwsService zjckFlwsService;

@Override

public void handleRequest(Request req) {

if(req.getRequestType()==RequestType.FLWSNRQS && !req.isHandled()){

//规则匹配后的处理

}

super.handleRequest(req);

}

}

实现心得

代码实现相对简单,但是将业务逻辑规则和控制任务代码进行了抽象隔离,符合OOP中的单一职责和开闭原则.控制线程还能很方便的放入到线程池中进行并发执行,提高了系统运行效率.也是将业务逻辑和控制逻辑解耦思想的体现.

RESTFUL API 设计实践总结

Posted on 2023-02-05

restful API 的设计之道

不同场景下数据迁移方案的选型分析

Posted on 2023-01-01

背景

公司在进行技术体系迁移,由原来的.net+sqlserver迁移到现在的java+mysql微服务体系.

这个大工程的实施是按子系统拆分逐步实现的,业务开发完后,通过流量网关切换流量到目标服务.由于服务之间的依赖,在现有运行期间,存在服务互相调用,数据互相同步的场景.这里着重说数据同步,数据迁移的解决方案.

上线方式是通过微服务逐步替换掉线上的.net服务,而原有.net体系内还有数据依赖,停掉.net服务后,数据还要从mysql同步回写到sqlserver.当依赖的服务也被替换下线后,数据同步服务就需要下线,不能依赖业务.为此,我从全量迁移,增量迁移,业务强依赖三个指标展开.ETL加载策略(增量/全量/流式)

场景一.对业务逻辑依赖性强,同步服务发生在单条数据修改时需要同步,方式灵活,对数据自定义要求较高.

解决方案:dts(数据传输服务)

优点:DTS服务是java微服务,主要是通过消费rabbitmq消息通知实现数据同步请求,client端在业务代码中插入consumer代码,所有的数据同步消息都会集中到rabbitmq中,由dts逐步消费,及时服务挂了,rabbitmq也能缓存这些未做任务.

缺点:dts的性能要求较高,在实际生产环境中,经常出现dts服务宕机,尤其是在同步高峰期,dts的流量经常被打满.因为dts消费完消息后,要往各个数据源同步执行CRUD操作.频繁的建立和销毁连接.

场景二.对业务逻辑依赖弱.跨数据源,实时性要求低,快速实现表的全量同步.

解决方案:datax(离线数据全量同步)

优点:适合基础字典表,例如类目,栏目或者被依赖的数据字典表同步,由datax job执行脚本,可以编写复杂的SQL同步语句,程序自定义程度强,较为灵活,支持主流的数据源,跨平台跨语言,其设置的schedule模式后,无需人工干预自动执行,后期服务下线也方便,支持多job执行模式.

  • 实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。

缺点:同步实时性不强,基于存量数据的同步.

场景三:对业务逻辑依赖弱,跨数据源,实时性要求高,快速实现表的增量同步

解决方案:alibaba canal(在线数据增量同步)

优点:实时性强,基于mysql的binlog日志流读取,同步到其他数据源,方便业务无关性,通常无需程序员介入.

缺点:实时性要求对性能要求较高,在非集群模式下,单台服务8G内存,10个管道就有点吃不消了.

参考文档

datax教程-入门

streamset教程

常用的ETL工具方法

alibaba canal

如何解决毫无头绪的线上问题

Posted on 2022-11-12

线上问题分类:功能性问题,性能性问题.也就是说,一个能直接看到症状,一个看不到症状.

按医学诊断流程:判断科目,收集指标,分析指标,预判故障,验证故障,解决原理,给出方案,实施方案,检查结果.

一.收集信息

能直接看到症状的问题都不是大问题,基本上可以通过日志,梳理流程,判断是哪部分出现的bug.

看不到症状的问题,比如响应时间越来越长,QPS越来越低,隔三差五就宕机.这种非功能性的bug.对问题进行分类:

是否资源不够用? CPU/内存/磁盘/数据库/网络带宽/JVM堆内存.

为什么不够用?初始太小?逐步填满?没有释放?就需要一个检测工具对当前的系统进行分析.

常用的有visualvm/arthas/jstack/jconsole.

elastic search 全文检索知识学习

Posted on 2022-08-29

如何自学一门中间件

1.去官网找文档,guide/reference/api,文档通读一遍,搞清楚是什么?

2.不要去看源码,而是先demo一遍,知道流程和步骤,抽象模型,核心组件,运作原理.搞清楚为什么工具能达到目的?

3.场景是什么?结合场景和demo练习几次,想想是怎么回事?如何做到的?

4.越是低版本越是接近底层原理,越是接近底层就越能弄明白,其他API无非就是各语言的扩展.比如掌握了命令行,其他语言API就不用记

class1 es安装,配置

1.图形化工具 elastic search head,kibana

class2 es的基本概念

集群,节点

索引,类型,文档,分片,映射

关系型数据库(mysql) 面向文档elasticsearch
database indices
tables types
rows documents
dolumns fields

逻辑设计:

一个索引类型中,包含多个文档,当我们索引一篇文档时,可以通过这样的规则: 索引>类型>文档id,通过这个组合我们能索引到具体的文档.

索引

文档

文档包含字段和值,可以层级包含字段和值,JSON格式.

字段类型

类型是包含文档的上级节点,类型中对字段的定义是映射

==倒排索引??==

class3 ik分词器详解

安装分词器插件 :

ik_smart 最细粒度划分 / ik_max_word 穷尽词库可能

elastic search analysis ik

安装kibana

查看分词器效果

1
2
3
4
5
6
7
8
9
kibana

GET _analyze{

"analyzer":"ik_smart",

"text":"中国共产党人"

}

自定义分词器插件

自己加字典,如何自己加字典?

添加字典:xxx.dic

修改到: ${es_plugins}/ik/config/IkAnalyzer.cfg.xml

class4 rest 分格的API

数据类型

添加文档字段时,可以指定也可以不指定数据类型

字符串类型

text,keyword

数值类型

long,integer,short,byte,double,float,half_float

日期类型

date

boolean类型

boolean

二进制类型

binary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
建立索引的数据结构存储规则
put http://127.0.0.1:9200/{index}
{
"mappings":{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"long"
},
"birthday":{
"type":"date"
}
}
}
}

索引的操作API

http method name url param
put 创建一个索引,添加规则 http://127.0.0.1:9200/{index} json body
get 查看所有索引 http://127.0.0.1:9200/_cat/indices?v
get _cat/xxx
get -cluster/xxx
get 查看单个索引 http://127.0.0.1:9200/{index}
delete 删除一个索引 http://127.0.0.1:9200/{index}
post 创建一个默认文档 http://127.0.0.1:9200/{index}/_doc
get 查看默认文档 http://127.0.0.1:9200/{index}/_doc/{doc_id}
put 修改默认文档[不建议] 覆盖式 http://127.0.0.1:9200/{index}/_doc/{doc_id}
post 修改默认文档[单字段修改] http://127.0.0.1:9200/{index}/_doc/{doc_id}/__update
delete 删除默认文档 http://127.0.0.1:9200/{index}/_doc/{doc_id}

文档的操作API

http method name url param
put 创建文档 http://127.0.0.1:9200/{index}/{type}/{document_id}
post 创建随机id文档 http://127.0.0.1:9200/{index}/{type}
get 获取文档 http://127.0.0.1:9200/{index}/{type}/{document_id}
post 更新文档字段 http://127.0.0.1:9200/{index}/{type}/{doc_id}/__update
get 简单匹配查询文档 http://127.0.0.1:9200/{index}/{type}/_search
delete 删除文档 http://127.0.0.1:9200/{index}/{type}/{document_id}
delete 条件删除文档 http://127.0.0.1:9200/{index}/{type}/_delete_by_query
get 查询所有文档 http://127.0.0.1:9200/{index}/{type}/_search 参数不同
get 字段匹配查询 http://127.0.0.1:9200/{index}/{type}/_search 参数不同
get 关键字精确查询 http://127.0.0.1:9200/{index}/{type}/_search 参数不同
get 多关键字精确查询 http://127.0.0.1:9200/{index}/{type}/_search 参数不同
get 指定查询字段 http://127.0.0.1:9200/{index}/{type}/_search
get 过滤字段 http://127.0.0.1:9200/{index}/{type}/_search
get 组合查询 http://127.0.0.1:9200/{index}/{type}/_search
get 范围查询 http://127.0.0.1:9200/{index}/{type}/_search
get 模糊查询 http://127.0.0.1:9200/{index}/{type}/_search
get 单字段排序 http://127.0.0.1:9200/{index}/{type}/_search
get 多字段排序 http://127.0.0.1:9200/{index}/{type}/_search
高亮查询 http://127.0.0.1:9200/{index}/{type}/_search highlight
分页查询 http://127.0.0.1:9200/{index}/{type}/_search
聚合查询 http://127.0.0.1:9200/{index}/{type}/_search
桶聚合查询 http://127.0.0.1:9200/{index}/{type}/_search

映射操作API

ttp method name url param
put 添加映射 http://127.0.0.1:9200/{index}/_mapping
get 查看映射 http://127.0.0.1:9200/{index}/_mapping
put 索引映射关联 http://127.0.0.1:9200/{index}/{type}/{doc_id}/__update

分词器类型

java rest client API

集成思路:

一.java rest sdk就是运用java httpclient把原始的命令行工具封装到request/response/client框架内,每个命令一组request/response,这是一个典型的http restful请求java框架. jar名称:elasticsearch-rest-high-level-client.jar,核心操作对象:TransportClient.java/RestHighLevelClient.java

二.要想跟spring集成,就是把对象实例托管到Context中,spring有spring-data.jar抽象,为elasticsearch提供具体的实现:spring-data-elasticsearch.jar, 核心操作对象:ElasticsearchRestTemplate.java

三.为了能实现自动化配置,用户无需做对象实例化,跟springboot集成autoconfigure模块,springboot把elasticsearch集成到了autoconfigure中,jar名称:spring-boot-starter-data-elasticsearch.jar,核心操作对象没变,只需要在application.yml中配置es的连接信息,就能直接使用elasticsearchRestTemplate.java

class5 springboot集成es

1
2
3
4
5
6
7
8
9
10
11
12
13
14
自动配置包:
maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

spring-boot-autoconfigure.jar

核心包
springframework.boot.autoconfigure.data.elasticsearch
核心类
RestHightLevelClient.java
RestLevelClientConfiguration.java

class6 京东爬虫入库es,全文检索

爬虫

1.写爬虫,根据京东搜索关键字的url去加载查询结果,放到list中.

2.将搜索到结果加入到es的文档库.输入关键字,返回添加是否成功.

3.获取这些数据实现搜索功能,关键字,分页,返回列表.

/条件搜索/ searchRequest SearchSourceBuilder

/分页 sourcebuilder.from(); size()

/精准匹配 TermQueryBuilder sourcebuilder.query(tempquerybuilder); sourcebuilder.timeout

//高亮关键字

/执行搜索

searchRequest.source(sourcebuilder);

client.search(request,requestOptions.default); //SearchResponse

//解析结果

SearchResponse.getHits().getHits().getSourceAsMap();//Map<String,Object>

搜索高亮

​ 高亮关键字/执行搜索/解析结果 //将原来字段替换为高亮的字段

举一反三

要做搜索,只需要把MySQL的数据加入到es,实现搜索全文高亮就行.

FAQ

1.服务启动后es连接失败

java.net.ConnectException: Connection refused

原因:client的jar包版本与server的版本不匹配导致连接失败.client是7.x 而我的服务是6.x

2.批量添加报错:

nested exception is org.elasticsearch.action.ActionRequestValidationException: Validation Failed: 1: type is missing;2: type is missing;3: type is missing;4: type is missing;5: type is missing;6: type is missing;7: type is missing

原因:6.x版本的es,插入索引需要 指定type,7.x之后就不需要了,这是版本差异.

index/type/doc_id

小结

1.es的发展历史

2.es的文件结构

3.安装es

4.生态圈,logstash,kibana

5.分词器ik,自定义分词器

6.restful API操作

7.javaAPI操作

8.springboot集成elasticsearch

9.爬虫爬取jd的关键字搜索

10.模拟全文检索

爬虫->加入es库->搜索es库返回list->高亮显示返回关键字

参考资料

使用Java API实现ES中的索引、映射、文档操作

低版本的elasticsearch中文 guide

springboot多数据源实现

Posted on 2022-08-02

一.多数据库实例集成

原理:

springboot支持与mybatis和mybatis-plus的快速自动化配置starter.

需要初始化定制DataSource,DataSourceTransactionManager,SqlSessionFactory三个对象的实例化.

基于扫描不同package下的mapper和mapper.xml创建上述的实例.

其他service层/mapper层跟单独数据源操作写法相同.只是要注意,这是通过package来隔离不同datasource,所以最好把不同的DataSource操作的service/mapper/entity放到各自的业务package中.

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main/java
com.mybatis.spring.demo
cms
entity
mapper
service
open
entity
mapper
service
main/resources
mapper
cms
xxxMapper.xml
open
yyyMapper.xml

单数据源实例化方式:只需要配置:

1
2
3
4
5
//application.properties
spring.datasource.url=xxx
spring.datasource.username=xxx
spring.datasource.password=xxx
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

而多数据源实例话方式需要配置各个数据源的jdbc连接

1
2
3
4
5
6
7
8
9
10
//application.properties
spring.datasource.cms.url=jdbc:mysql:xxx
spring.datasource.cms.username=xxx
spring.datasource.cms.password=xxx
spring.datasource.cms.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.open.url=jdbc:mysql:xxx
spring.datasource.open.username=xxx
spring.datasource.open.password=xxx
spring.datasource.open.driver-class-name=com.mysql.cj.jdbc.Driver

创建数据源配置类

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/**
* MyBatis 在 SpringBoot 中动态多数据源配置
* @author sheng
* @date 2022-08-02
* */
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.open")
@MapperScan(basePackages = "com.mybatis.spring.demo.open.mapper", sqlSessionFactoryRef = "openSqlSessionFactory")
@Setter
public class OpenDbConfig {

@Autowired
private DBProperties dbProperties;

// 数据库配置息信息
private String driverClassName;
private String url;
private String username;
private String password;

@Bean(name = "openDataSource")
public DataSource unionDataSource() {
HikariDataSource dataSource = new HikariDataSource();
// 设置数据源信息
dataSource.setDriverClassName(driverClassName);
dataSource.setJdbcUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
// 公共属性配置
dataSource.setMinimumIdle(dbProperties.getMinimumIdle());
dataSource.setIdleTimeout(dbProperties.getIdleTimeout());
dataSource.setMaximumPoolSize(dbProperties.getMaximumPoolSize());
dataSource.setAutoCommit(dbProperties.isAutoCommit());
dataSource.setPoolName("open_" + dbProperties.getPoolName());
dataSource.setMaxLifetime(dbProperties.getMaxLifetime());
dataSource.setConnectionTimeout(dbProperties.getConnectionTimeout());
dataSource.setConnectionTestQuery(dbProperties.getConnectionTestQuery());
return dataSource;
}

/**
* union 事务管理器
*
* @return
*/
@Role(100)
@Bean(name = CmsConst.TRANSACTION_OPEN)
public DataSourceTransactionManager unionTransactionManager() {
return new DataSourceTransactionManager(unionDataSource());
}
/**
* unionDataSource Session工厂类
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean(name = "openSqlSessionFactory")
public SqlSessionFactory unionSqlSessionFactory(@Qualifier("openDataSource") DataSource dataSource)
throws Exception {
//如果是与mybatis-plus集成就需要使用MybatisSqlSessionFactoryBean来代理实例化SqlSessionFactory
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
MybatisConfiguration config = new MybatisConfiguration();
config.setMapUnderscoreToCamelCase(true);
sessionFactory.setConfiguration(config);
//如果不使用xml的方式配置mapper,则可以省去下面这行mapper location的配置。
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/open/*.xml"));
return sessionFactory.getObject();
}

同理,其他的数据源配置文件只需要修改

1
2
3
4
5
6
7
8
//配置参数前缀
@ConfigurationProperties(prefix = "spring.datasource.cms")
//扫描包路径
@MapperScan(basePackages = "com.mybatis.spring.demo.cms.mapper", sqlSessionFactoryRef = "cmsSqlSessionFactory")
//Xml文件路径

//如果不使用xml的方式配置mapper,则可以省去下面这行mapper location的配置。
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/cms/*.xml"));

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

FAQ问题

1.Spring Boot集成MyBatis报错:Invalid bound statement (not found)…解决方案

无法识别mybatis-plus包的BaseMapper通用CURD方法

一.SqlSessionFactory不要使用原生的,请使用MybatisSqlSessionFactory

2.自定义 SQL 无法执行

1
2
3
4
5
application.properties
##mybatis-plus mapper xml 文件地址
mybatis-plus.mapper-locations= classpath*:/mapper/**/*Mapper.xml
##mybatis-plus type-aliases 文件地址
mybatis-plus.type-aliases-package= com.mybatis.spring.demo

参考资料

编程思维-抽象思维训练方法论

Posted on 2022-05-15
程序员必备的思维能力:抽象思维

转载自: 原文 https://mp.weixin.qq.com/s/cJ0odiYcphhNBoAVjqpCZQ

若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。——大卫·林奇

抽象思维是我们工程师最重要的思维能力。因为软件技术 本质上就是一门抽象的艺术。我们的工作是存思维的“游戏”,虽然我们在使用键盘、显示器,打开电脑可以看到主板、硬盘等硬件。但我们即看不到程序如何被执行,也看不到0101是如何被CPU处理的。

我们工程师每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理。从而抽象出各种概念,挖掘概念和概念之间的关系,对问题域进行建模,然后通过编程语言实现业务功能。所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念,当然,也包括尝试看懂那些“该死的、别人写的”代码。

在我接触的工程师中,能深入理解抽象概念的并不多,能把抽象和面向对象、架构设计进行有机结合,能用抽象思维进行问题分析、化繁为简的同学更是凤毛麟角。

对于我本人而言,每当我对抽象有进一步的理解和认知,我都能切身感受到它给我在编码和设计上带来的质的变化。同时感慨之前对抽象的理解为什么如此肤浅。如果时间可以倒流的话,我希望我在我职业生涯的早期,就能充分意识到抽象的重要性,能多花时间认真的研究它,深刻的理解它,这样应该可以少走很多弯路。

1.1 什么是抽象

关于抽象的定义,百度百科是这样说的:

抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。
实际上,抽象是与具体相对应的概念,具体是事物的多种属性的总和,因而抽象亦可理解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维活动。[1]

Wikipedia的解释是:

抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个皮质的足球,我们可以过滤它的质料等信息,得到更一般性的概念,也就是球。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。[2]

简单而言,“抽”就是抽离,“象”就是具象,字面上理解抽象,抽象的过程就是从“具象”事物中归纳出共同特征,“抽取”得到一般化(Generalization)的概念的过程。英文的抽象——abstract来自拉丁文abstractio,它的原意是排除、抽出。

为了更好的方便你理解抽象,让我们先来看一幅毕加索的画,如下图所示,图的左边是一头水牛,是具象的,右边是毕加索画,是抽象的。怎么样,是不是感觉自己一下子理解了抽象画的含义。 图片

可以看到,抽象牛只有几根线条,不过这几根线条是做了高度抽象之后的线条,过滤了水牛的绝大部分细节,保留了牛最本质特征,比如牛角,牛头,牛鞭、牛尾巴等等。这种对细节的舍弃使得“抽象牛”具有更好的泛化(Generalization)能力。可以说,抽象更接近问题的本质。也就是说所有的牛都逃不过这几根线条。

1.2 抽象和语言是一体的

关于抽象思维,我们在百度百科上可以看到如下的定义:

抽象思维,又称词(概念)的思维或者逻辑思维,是指用词(概念)进行判断、推理并得出结论的过程。抽象思维以词(概念)为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。[3]

之所以把抽象思维称为词思维或者概念思维,是因为语言和抽象是一体的。当我们说“牛”的时候,说的就是“牛”的抽象,他代表了所有牛共有的特征。同样,当你在程序中创建Cow这个类的时候,道理也是一样。在生活中,我们只见过一头一头具象的牛,“牛”作为抽象的存在,即看不见也摸不着。

这种把抽象概念作为世界本真的看法,也是古希腊哲学家柏拉图的最重要哲学思想。柏拉图认为,我们所有用感觉感知到的事物,都源于相应的理念。他认为具体事物的“名”,也就是他说的“理念世界”才是本真的东西,具体的一头牛,有大有小,有公有母,颜色、性情、外形各自不同。因此我们不好用个体感觉加以概括,但是这些牛既然都被统称为“牛”,则说明它们必然都源于同一个“理念”,即所谓“牛的理念”或者“理念的牛”,所以它们可以用“牛”加以概括。尚且不论“理念世界”是否真的存在,这是一个哲学问题,但有一点可以确定,我们的思考,对概念的表达都离不开语言。[4]

这也是为什么,我在做设计和代码审查(Code Review)的时候,会特别关注命名是否合理的原因。因为命名的好坏,在很大程度上反应了我们对一个概念的思考是否清晰,我们的抽象是否合理,反应在代码上就是,代码的可读性、可理解性是不是良好,以及我们的设计是不是到位。

有人做过一个调查,问程序员最头痛的事情是什么,通过Quora和Ubuntu Forum的调查结果显示,程序员最头疼的事情是命名。如果你曾经为了一个命名而绞尽脑汁,就不会对这个结果感到意外。

就像Stack Overflow的创始人Joel Spolsky所说的:“起一个好名字应该很难,因为,一个好名字需要把要义浓缩在一到两个词。(Creating good names is hard, but it should be hard, because a great name captures essential meaning in just one or two words)。”

是的,这个浓缩的过程就是抽象的过程。我不止一次的发现,当我觉得一个地方的命名有些别扭的时候,往往就意味着要么这个地方我没有思考清楚,要么是我的抽象弄错了。

关于如何命名,我在《代码精进之路》里已经有比较详尽的阐述,这里就不赘述了。

我想强调的是语言是明晰概念的基础,也是抽象思维的基础,在构建一个系统时,值得我们花很多时间去斟酌、去推敲语言。在我做过的一个项目中,就曾为一个关键实体讨论了两天,因为那是一个新概念,尝试了很多名字,始终感觉到别扭、不好理解。随着我们讨论的深入,对问题域理解的深入,我们最终找到了一个相对比较合适的名字,才肯罢休。

这样的斟酌是有意义的,因为明晰关键概念,是我们设计中的重要工作。虽然不合理的命名,不合理的抽象也能实现业务功能。但其代价就是维护系统时,极高的认知负荷。随着时间的推移,就没人能搞懂系统的设计了。

1.3 抽象的层次性

回到毕加索的抽象画,如下图所示,如果映射到面向对象编程,抽象牛就是抽象类(Abstract Class),代表了所有牛的抽象。抽象牛可以泛化成更多的牛,比如水牛、奶牛、牦牛等。每一种牛都代表了一类(Class)牛,对于每一类牛,我们可以通过实例化,得到一头具体的牛实例(Instance)。 图片

从这个简单的案例中,我们可以到抽象的三个特点:

  1. 第一,抽象是忽略细节的。抽象类是最抽象的,忽略的细节也最多,就像抽象牛,只是几根线条而已。在代码中,这种抽象可以是Abstract Class,也可以是Interface。
  2. 第二,抽象代表了共同性质。类(Class)代表了一组实例(Instance)的共同性质,抽象类(Abstract Class)代表了一组类的共同性质。对于我们上面的案例来说,这些共同性质就是抽象牛的那几根线条。
  3. 第三,抽象具有层次性。抽象层次越高,内涵越小,外延越大,也就是说它的涵义越小,泛化能力越强。比如,牛就要比水牛更抽象,因为它可以表达所有的牛,水牛只是牛的一个种类(Class)。

抽象的这种层次性,是除了抽象概念之外,另一个我们必须要深入理解的概念,因为小到一个方法要怎么写,大到 一个系统要如何架构,以及我们后面第三章要介绍的结构化思维,都离不开抽象层次的概念。

在进一步介绍抽象层次之前,我们先来理解一下外延和内涵的意思:

抽象是以概念(词语)来反映现实的过程,每一个概念都有一定的外延和内涵.概念的外延就是适合这个概念的一切对象的范围,而概念的内涵就是这个概念所反映的对象的本质属性的总和.例如“平行四边形”这个概念,它的外延包含着一切正方形、菱形、矩形以及一般的平行四边形,而它的内涵包含着一切平行四边形所共有的“有四条边,两组对边互相平行”这两个本质属性。

一个概念的内涵愈广,则其外延愈狭;反之,内涵愈狭,则其外延愈广。例如,“平行四边形”的内涵是“有四条边,两组对边互相平行”,而“菱形”的内涵除了这两条本质属性外,还包含着“四边相等”这一本质属性。“菱形”的内涵比“平行四边形”的内涵广,而“菱形”的外延要比“平行四边形”的外延狭。

所谓的抽象层次就体现在概念的外延和内涵上,这种层次性,基本可以体现在任何事物上,比如一份报纸就存在多个层次上的抽象,“出版品”最抽象,其内涵最小,但外延最大,“出版品”可以是报纸也可以是期刊杂志等。

  1. 一个出版品
  2. 一份报纸
  3. 《旧金山纪事报》
  4. 5 月 18 日的《旧金山纪事报》

当我要统计美国有多少个出版品,那么就要用到最上面第一层“出版品”的抽象,如果我要查询旧金山5月18日当天的新闻,就要用到最下面第四层的抽象。

每一个抽象层次都有它的用途,对于我们工程师来说,如何拿捏这个抽象层次是对我们设计能力的考验,抽象层次太高和太低都不行。

比如,现在要写一个水果程序,我们需要对水果进行抽象,因为水果里面有红色的苹果,我们当然可以建一个RedApple的类,但是这个抽象层次有点低,只能用来表达“红色的苹果”。来一个绿色的苹果,你还得新建一个GreenApple类。

为了提升抽象层次,我们可以把RedApple类改成Apple类,让颜色变成Apple的属性,这样红色和绿色的苹果就都能表达了。再继续往上抽象,我们还可以得到水果类、植物类等。再往上抽象就是生物、物质了。

你可以看到,抽象层次越高,内涵越小,外延越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。 图片

具体要抽象到哪个层次,要视具体的情况而定了,比如这个程序是专门研究苹果的可能到Apple就够了,如果是卖水果的可能需要到Fruit,如果是植物研究的可能要到Plant,但很少需要到Object。

我经常开玩笑说,你把所有的类都叫Object,把所有的参数都叫Map的系统最通用,因为Object和Map的内涵最小,其延展性最强,可以适配所有的扩展。从原理上来说,这种抽象也是对的,万物皆对象嘛。但是这种抽象又有什么意义呢?它没有表达出任何想表达的东西,只是一句正确的废话而已。

越抽象,越通用,可扩展性越强,然而其语义的表达能力越弱。越具体,越不好延展,然而其语义表达能力很强。所以,对于抽象层次的权衡,是我们系统设计的关键所在,也是区分普通程序员和优秀程序员的关键所在。

1.4 软件中的分层抽象无处不在

越是复杂的问题越需要分层抽象,分层是分而治之,抽象是问题域的合理划分和概念语义的表达。不同层次提供不同的抽象,下层对上层隐藏实现细节,通过这种层次结构,我们才有可能应对像网络通信、云计算等超级复杂的问题。

网络通信是互联网最重要的基础实施,但同时它又是一个很复杂的过程,你要知道把数据包传给谁——IP协议,你要知道在这个不可靠的网络上出现状况要怎么办——TCP协议。有这么多的事情需要处理,我们可不可以在一个层次中都做掉呢?当然是可以的,但显然不科学。因此,ISO制定了网络通信的七层参考模型,每一层只处理一件事情,低层为上层提供服务,直到应用层把HTTP,FTP等方便理解和使用的协议暴露给用户。 图片

编程语言的发展史也是一个典型的分层抽象的演化史。

机器能理解的只有机器语言,即各种二进制的01指令。如果我们采用O1的输入方式,其编程效率极低(学过数字电路的同学,体会下用开关实现加减法)。所以我们用汇编语言抽象了二进制指令。然而汇编还是很底层,于是我们用C语言抽象了汇编语言。而高级语言Java是类似于C这样低级语言的进一步抽象,这种逐层抽象极大的提升了我们的编程效率。 图片

1.5 重复代码是抽象的缺失

如果说抽象的本质是共性的话,那么我们代码中的重复代码,是不是就意味着抽象的缺失呢?

是这样的,重复代码是典型的代码坏味道,其本质问题就是抽象的缺失。因为我们Ctrl+C加Ctrl+V的工作习惯,导致没有对共性代码进行抽取,或者虽然抽取了,只是简单的用了一个Util名字,没有给到一个合适的名字,没有正确的反应这段代码所体现的抽象概念,都属于抽象不到位。

有一次,我在Review团队代码的时候,发现有一段组装搜索条件的代码,在几十个地方都有重复。这个搜索条件还比较复杂,是以元数据的形式存在数据库中,因此组装的过程是这样的:

  • 首先,我们要从缓存中把搜索条件列表取出来;
  • 然后,遍历这些条件,将搜索的值填充进去;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//取默认搜索条件
List<String> defaultConditions = searchConditionCacheTunnel.getJsonQueryByLabelKey(labelKey);
for(String jsonQuery : defaultConditions){
jsonQuery = jsonQuery.replaceAll(SearchConstants.SEARCH_DEFAULT_PUBLICSEA_ENABLE_TIME, String.valueOf(System.currentTimeMillis() / 1000));
jsonQueryList.add(jsonQuery);
}
//取主搜索框的搜索条件
if(StringUtils.isNotEmpty(cmd.getContent())){
List<String> jsonValues = searchConditionCacheTunnel.getJsonQueryByLabelKey(SearchConstants.ICBU_SALES_MAIN_SEARCH);
for (String value : jsonValues) {
String content = StringUtil.transferQuotation(cmd.getContent());
value = StringUtil.replaceAll(value, SearchConstants.SEARCH_DEFAULT_MAIN, content);
jsonQueryList.add(value);
}
}

简单的重构无外乎就是把这段代码提取出来,放到一个Util类里面给大家复用。然而我认为这样的重构只是完成了工作的一半,我们只是做了简单的归类,并没有做抽象提炼。

简单分析,不难发现,此处我们是缺失了两个概念:一个是用来表达搜索条件的类——SearchCondition;另一个是用来组装搜索条件的类——SearchConditionAssembler。只有配合命名,显性化的将这两个概念表达出来,才是一个完整的重构。

重构后,搜索条件的组装会变成一种非常简洁的形式,几十处的复用只需要引用SearchConditionAssembler就好了。

1
2
3
4
5
6
7
public class SearchConditionAssembler {
public static SearchCondition assemble(String labelKey){
String jsonSearchCondition = getJsonSearchConditionFromCache(labelKey);
SearchCondition sc = assembleSearchCondition(jsonSearchCondition);
return sc;
}
}

由此可见,提取重复代码只是我们重构工作的第一步。对重复代码进行概念抽象,寻找有意义的命名才是我们工作的重点。

因此,每一次遇到重复代码的时候,你都应该感到兴奋,想着,这是一次锻炼抽象能力的绝佳机会,当然,测试代码除外。

1.6 强制类型转换是抽象层次有问题

面向对象设计里面有一个著名的SOLID原则是由Bob大叔(Robert Martin)提出来的,其中的L代表LSP,就是Liskov Substitution Principle(里氏替换原则)。简单来说,里氏替换原则就是子类应该可以替换任何父类能够出现的地方,并且经过替换以后,代码还能正常工作。

思考一下,我们在写代码的过程中,什么时候会用到强制类型转换呢?当然是LSP不能被满足的时候,也就是说子类的方法超出了父类的类型定义范围,为了能使用到子类的方法,只能使用类型强制转换将类型转成子类类型。

举个例子,在苹果(Apple)类上,有一个isSweet()方法是用来判断水果甜不甜的;西瓜(Watermelon)类上,有一个isJuicy()是来判断水分是否充足的;同时,它们都共同继承一个水果(Fruit)类

此时,我们需要挑选出甜的水果和有水分的习惯,我们会写一个如下的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FruitPicker {

public List<Fruit> pickGood(List<Fruit> fruits){
return fruits.stream().filter(e -> check(e)).
collect(Collectors.toList());
}

private boolean check(Fruit e) {
if(e instanceof Apple){
if(((Apple) e).isSweet()){
return true;
}
}
if(e instanceof Watermelon){
if(((Watermelon) e).isJuicy()){
return true;
}
}
return false;
}
}

因为pick方法的入参的类型是Fruit,所以为了获得Apple和Watermelon上的特有方法,我们不得不使用instanceof做一个类型判断,然后使用强制类型转换转成子类类型,以便获得他们的专有方法,很显然,这是违背了里式替换原则的。

这里问题出在哪里?对于这样的代码我们要如何去优化呢?仔细分析一下,我们可以发现,根本原因是因为isSweet和isJuicy的抽象层次不够,站在更高抽象层次也就是Fruit的视角看,我们挑选的就是可口的水果,只是具体到苹果我们看甜度,具体到西瓜我们看水分而已。

因此,解决方法就是对isSweet和isJuicy进行抽象,并提升一个层次,在Fruit上创建一个isTasty()的抽象方法,然后让苹果和西瓜类分别去实现这个抽象方法就好了。

图片

下面是重构后的代码,通过抽象层次的提升我们消除了instanceof判断和强制类型转换,让代码重新满足了里式替换原则。抽象层次的提升使得代码重新变得优雅了。

1
2
3
4
5
6
7
8
9
10
11
12
public class FruitPicker {

public List<Fruit> pickGood(List<Fruit> fruits){
return fruits.stream().filter(e -> check(e)).
collect(Collectors.toList());
}

//不再需要instanceof和强制类型转换
private boolean check(Fruit e) {
return e.isTasty();
}
}

所以,每当我们在程序中准备使用instanceof做类型判断,或者用cast做强制类型转换的时候。每当我们的程序不满足LSP的时候。你都应该警醒一下,好家伙,这又是一次锻炼抽象能力的绝佳机会。

1.7 如何提升抽象思维能力

抽象思维能力是我们人类特有的、与生俱来的能力,除了上面说的在编码过程中可以锻炼抽象能力之外,我们还可以通过一些其他的练习,不断的提升我们的抽象能力。

多阅读

为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面的时候会将你的大脑铺满,较少需要抽象和想象。

这也是为什么我们不提倡让小孩子过多的暴露在电视或手机屏幕前的原因,因为这样不利于他抽象思维的锻炼。

抽象思维的差别让孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的,就去读技校了,因为技校比大学会更加具象:车铣刨磨、零部件都能看得见摸得着。体力劳动要比脑力劳动来的简单。

多总结沉淀

小时候不理解,语文老师为什么总是要求我们总结段落大意、中心思想什么的。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维能力。

记录也是很好的总结习惯。就拿读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳书中的内容,这样不仅可以加深理解,而且还可以提升自己的抽象思维能力。

我从四年前开始系统的记录笔记,做总结沉淀,构建自己的知识体系。这种思维训练的好处显而易见,可以说我之前写的《从码农到工匠》和现在正在写的《程序员必备的思维能力》都离不开我总结沉淀的习惯。

命名训练

每一次的变量命名、方法命名、类命名都是一次难得的抽象思维训练机会,前面已经说过了,语言和抽象是一体的,命名的好坏直接反应了我们的问题域思考的是否清晰,反应了我们抽象的是否合理。

现实情况是,我们很多的工程师常常忽略了命名的重要性,只要能实现业务功能,名字从来就不是重点。

实际上,这是对系统的不负责任,也是对自己的不负责任,更是对后期维护系统的人不负责任。写程序和写文章有很大的相似性,本质上都是在用语言阐述一件事情。试想下,如果文章中用的都是些词不达意的句子,这样的文章谁能看得懂,谁又愿意去看呢。

同样,我一直强调代码要显性化的表达业务语义,其中命名在这个过程中扮演了极其重要的角色。为了代码的可读性,为了系统的长期可维护性,为了我们自身抽象思维的训练,我们都不应该放过任何一个带有歧义、表达模糊、意不清的命名。

领域建模训练

对于技术同学,我们还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象的时候,当我们对领域进行划分和建模的时候,实际上也是在锻炼我们的抽象能力。

我们可以对自己工作中的问题域进行建模,当然也可以通过阅读一些优秀源码背后的模型设计来学习如何抽象、如何建模。比如,我们知道Spring的核心功能是Bean容器,那么在看Spring源码的时候,我们可以着重去看它是如何进行Bean管理的?它使用的核心抽象是什么?不难发现,Spring是使用了BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader等核心抽象实现了Bean的定义、获取和创建。抓住了这些核心抽象,我们就抓住了Spring设计主脉。

除此之外,我们还可以进一步深入思考,它为什么要这么抽象?这样抽象的好处是什么?以及它是如何支持XML和Annotation(注解)这两种关于Bean的定义的。

这样的抽象思维锻炼和思考,对提升我们的抽象能力和建模能力非常重要。关于这一点,我深有感触,初入职场的时候,当我尝试对问题域进行抽象和建模的时候,会觉得无从下手,建出来的模型也感觉很别扭。然而,经过长期的、刻意的学习和锻炼之后,很明显可以感觉到我的建模能力和抽象能力都有很大的提升。不但分析问题的速度更快了,而且建出来的模型也更加优雅了。

1.8 小结

  • 抽象思维是程序员最重要的思维能力,抽象的过程就是寻找共性、归纳总结、综合分析,提炼出相关概念的过程。
  • 语言和抽象是一体的,抽象思维也叫词思维,因为抽象的概念只能通过语言才能表达出来。
  • 抽象是有层次性的,抽象层次越高,内涵越小,外延越大,扩展性越好;反之,抽象层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。
  • 对抽象层次的拿捏,体现了我们的设计功力,视具体情况而定,抽象层次既不能太高,也不能太低。
  • 重复代码意味着抽象缺失,强制类型转换意味着抽象层次有问题,我们可以利用这些信号来重构代码,让代码重新变的优雅。
  • 我们可以通过刻意练习来提升抽象能力,这些练习包括阅读、总结、命名训练、建模训练等。

参考文档

[1] https://baike.baidu.com/item/抽象/9021828

[2] https://zh.wikipedia.org/wiki/抽象化

[3] https://baike.baidu.com/item/抽象思维

[4] https://www.sohu.com/a/359915387_260616

XX公司生产事故-高并发下访问redis一个key

Posted on 2021-07-05

故障分析:
在分布式服务部署环境,高并发访问时,一个seq生成到临界点了,单秒订单记录重复,然后引发了一系列的问题

问题代码:

1
2
3
4
5
6
7
8
9
public string getSeq(){
long seq=redisClient.incr(SN_REDIS_KEY);
if(seq>999999){
seq=1;
redisClient.del(SN_REDIS_KEY);
seq=redisClient.incr(SN_REDIS_KEY);
}
return string.format("%06d",seq);
}

原因:
这段代码是redis客户端执行del操作,下一步是incr操作,如果是在高并发场景下,没有加锁控制,会导致服务副本不安全执行上面的代码导致redis客户端返回的数据是脏数据,从而程序的执行逻辑出现异常.

解决:
这段代码如果是单体服务,要做进程内的加锁操作,可以使用sync(低效率简单),也可以使用可重入锁.
如果是在分布式多副本环境下,要做分布式锁操作,方案有三个:
使用zk实现分布式锁,在锁环境下操作上述代码.
使用redis实现全局原子性操作,redis可以分发分布式锁,redis可以执行lua脚本,保障全局下,这段代码操作的原子性.

秦老师的解决方案:
ringbuffer,imax方案,单机tps达到600万.

Untitled

Posted on 2021-03-18

title: 程序员-如何提升实力
date: 2021-03-19
tags: mind

作者:kimmking
链接:https://www.zhihu.com/question/315201616/answer/1756148937
来源:知乎
以下几个任何一个都可以超过90%程序员:
1.把事情想明白,说清楚,跟别人商量好
2.写代码,注意边界条件和编码规范,写单测,基本做到无bug提测
3.工作中做好计划和进度跟踪,沟通和汇报,不把问题遗留到变成事故
4.思考和分析,如何优化目前的工作流程,引入工具和方法,提升生产效率
5.把自己工作中用到的技术用熟,搞清楚原理,优点短处,适用场景
6.不断接触新技术思想和工具,完善自身知识体系结构
7.深入学习至少一个常用开源项目,源码层面系统掌握这项技术
8.持续坚持学习和技术内容输出,每个星期产出2篇原创技术文章

编程思想-解耦实践

Posted on 2020-11-12

场景分类介绍

[TOC]

场景分类

责任链模式解耦

案例:当出现要用excel的规则逻辑来设计对同一个数据输入源进行处理时,发现规则而逻辑可以和线程控制分离,而这个分离器就是责任链模式,参考了汽车车轮和发动机直接用离合器的思想,再往下延伸离合器可以是手动挡,也可以是自动挡,自动挡离合器的原理是什么呢?

文件操作解耦

案例 cms的发布流程:模板文件和数据集成生成静态文件推送到ftp后,如何清理本地的副本文件.

初级方案:将删除文件流程加入到程序流程中,实现创建-传输-删除一体的事务管理,缺点,文件量大,过程较长导致线程占用资源较长,不利于高性能扩展,如果程序没有清理该文件将被忽略.

解耦方案:是将删除文件的操作交给定时任务执行shell,清除某个文件目录下的文件,直接批量删除,这样创建文件与删除文件就不用直接耦合到一起,清理文件批量高效,也不用占用IO资源.不会存在文件遗漏,而且在大批量文件操作性能更好.

mq实现解耦

案例:cms的失效页批量替换,也是涉及到批量文件的发布.

初级方案:如果全部在主线程中处理,将导致线程阻塞,主线程池无法及时释放会导致线程池爆仓,影响用户体验.

解耦方案:所以我采用了mq消息解耦,将批量文件替换的需求以消息形式发送,页面端请求线程快速返回.,用状态机来反馈处理结果.这样在处理大量的文件发布时,有消息队列堆积来完成消费,生产者只需要推消息.这种情况在电商系统中常用于秒杀,下单等高并发场景.

规则引擎,让控制和具体的业务逻辑解耦

没有具体案例,规则引擎也是一把双刃剑,适应场景有.

1、只有少量静态规则,并且不经常改变,不建议使用规则引擎;

2、需要在运行时动态修改规则,可考虑使用规则引擎;

3、在应用程序中需要通过可视化工具来修改规则,可考虑使用规则引擎;

4、规则需要非技术人员维护的情况下,可考虑使用规则引擎;

5、如果项目工期较紧,除非非常熟悉规则引擎,否则不建议使用;

6、如果使用Excel来管理规则,可考虑使用规则引擎;

xxl-job定时任务解耦

案例:将定时任务放入第三方中间件xxl-job中

初级方案:在服务中开启schedule,定时执行,这样的优点是当定时任务数量较少,单服有优势.对于分布式微服务集群场景就会有重复执行的bug.

解耦方案:避免了分布式集群下,任务重复执行的场景,job的生产端和执行端解耦,并发执行能力强.

常用的任务(定时发布主页/定时发布模块/定时创建新闻ES索引和文档入库)

数据同步操作解耦

案例:cms是有新老系统迁移的场景,在新闻业务上,有数据同步到老系统的业务,

初级方案:第一版本实现是将数据回写老系统直接耦合到业务逻辑,增加了业务复杂性.同步数据与业务逻辑耦合增加业务逻辑复杂性,而且不同的数据库操作保持一致性使用分布式事务非常麻烦.

解耦方案:数据同步这种跟业务逻辑关联并不大,只是数据同步要看需求:实时性/一致性/并发性/

同步操作会逐步退出业务逻辑,实时性要求并不高,只需要最终一致就行.就推荐使用streamset的dcd同步功能,使用易购系统,基于mysql的binlog日志实现数据的同步变更.

其他解耦方案:

在离线数据非实时同步场景,基于datax的离线数据同步

在业务复杂非实时场景:基于dts的实时在线数据同步

状态机解耦

案例:二手房贷款诈骗模型

需求背景:一批数据经过 税务局,房管局,银保监,公安局各个节点的逻辑过滤,打分(自有逻辑/外部服务数据同步打分),将高分者建立线索,通过对线索人员的社会轨迹和关系进行串并成嫌疑团伙,最后获得高分团伙.

初级方案:

按需求过程式编程,所有业务逻辑耦合,会导致业务逻辑复杂庞大,对局部的修改影响到全局,一旦某个节点无法提供数据整个程序无法执行.

解耦方案:

抽象实体:税务局处理,房管局处理,银保监处理,公安局处理.(工厂模式)

通用逻辑处理:获取数据列表->对数据进行评分->写入评分表.(模板模式)

控制器:获取待处理状态的数据批次->在工厂中获取列表->处理->更新数据批次状态,管理批次的节点任务执行状态.

这样一来,各个节点互相并不直接依赖,对单个的修改不会影响到其他工厂的变化(单一职责,开闭原则).

增加了控制器这个表和节点状态管理,将各个节点状态存储,执行任务要对状态进行判断.这样就实现了业务逻辑与控制的分离,如果要修改控制,不会对节点内的业务逻辑有影响,例如要变更控制逻辑,不需要动原有的业务代码,只需要在抽象父类工厂对控制方法变更.(模板方法的优势)

总结

通过上述解耦思想的案例场景可以得到,解耦的目的是为了可扩展性,对代码复杂度要求其实更高了,这是模块封装的一种思想.

123

盛富强

blogs and research

28 posts
17 tags
github
© 2023 盛富强
Powered by Hexo
|
Theme — NexT.Muse v5.1.4