liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

领域驱动设计-柔性设计

Posted on 2020-07-26 | Edited on 2022-09-21 | In 读后感 , 领域驱动设计

软件的最终目的是为客户服务,但是首先要服务于研发人员,一个设计的好的模型一定是是健壮,且易于扩展的。我们称这种设计为柔性设计.

首先、程序员看到接口就应该明白接口是做什么用的,没有任何的副作用。其次、设计涉及的模型要健壮,利于扩展增加功能,而不能每次修改就面临推倒重来

要求

  • INTENTION-REVEALING INTERFACES:接口、Entity、方法命名清晰无歧义,且符合整个模型的通用术语。避免调用者去深究实现含义。
  • SIDE-EFFECT-FREE FUNCTION:引起Entity状态改变的(需改操作)逻辑抽象出来放到ValueObject中去【service】中。否则调用方法后开发人员无法预知这个Entity的状态变化。
  • ASSERTION:要通过断言告知调用者所有的关键分支,如果java等不支持断言需要提供完备的unit test。
  • CONCEPTUALCONTOUR:确定模型的底层轮廓,尽量避免底层轮廓的修改(如果底层轮廓经常修改,说明模型不够健壮)
  • STANDALONE CLASS:减少Entity之间的耦合。【剔除无关的属性和功能】
  • CLOSER FOR OPERTATION:闭合操作,加强整个系统的内聚。【一个模型只做自己相关的事情】

领域驱动设计-初探(一)

Posted on 2020-07-13 | Edited on 2022-09-21 | In 读后感 , 领域驱动设计

问题

  • DDD是什么?为了解决什么问题?
  • 什么叫领域转件相当于我们什么角色?

DDD

DDD是领域驱动设计(Domain-Driven-Design),了解它首先我们要了解什么是领域模型呢。

我们在现实中遇到了一个问题,需要RD开发一个系统来解决它,这时候就需要将这个问题映射成一个抽象的模型,这个模型作为代码和现实的纽带被叫做领域模型。

我们的领域专家和研发工程师对领域模型不断的探讨,精简,逐渐的使其越来越抽象通用,逐步的使我们的系统更完善,功能更强大。

上述突出以下几点:

  1. 领域模型是现实和程序的纽带,他反应了一个具体的显示问题。
  2. 它需要被领域专家和研发讨论,所以应该有统一的双方都能理解的术语。
  3. 它不是一成不变的,在做加法的同时也需要在讨论中逐渐做减法。
  4. 它不单单是一个简单的数据集合,他还描述了领域模型间不同的关系。
  5. 它需要不断的演进。

统一术语

在领域模型中我们需要统一术语我们称他们为通用语言,他能减少领域专家和我们的研发的沟通成本,避免翻译带来的语义混淆。

两者是通过术语来沟通的:领域专家的模型—->术语<––研发的系统。

对术语的要求:

  1. 对于通用语言来说:模型和代码必须统一,比如模型定义了Goods,那么代码中要有相应的类Goods
  2. 通用语言需要讨论:我们在技术讨论中应该尽量用简短的通用语言来描述
  3. 要有不断完善的文档简短的文档来维护我们的通用语:
    1. 我们可以通过简短类图+类图备注进行描述,并且文档应该作为代码和模型的叩痛补充;
    2. 在模型演进中通过通用语言的完善来更迭文档;

JPA+Mongodb的Example查询不出数据的问题

Posted on 2020-07-08 | Edited on 2022-09-21 | In 经验积累 , 项目积累

现象

一次查询中发现一个诡异的问题,用jpa写入到mongodb的数据无法查询出来。

原因分析

查询采用了SpringCloudData的JPA+Mongodb的Example动态拼接查询条件,具体代码如下:

1
2
3
4
5
6
7
8
  public PageDTO<EmployeeInfoDTO> findByCondition(String name, String number, Integer pageNumber, Integer pageSize) {
EmployeeInfoDO query=new EmployeeInfoDO();
//......动态设置query权限
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.Direction.DESC, "gmtCreate");
Page<EmployeeInfoDO> ret = employeeInfoRepo.findAll(Example.of(query, ExampleMatcher.matchingAll()), pageable);
//......
return pageDTO;
}

但是这里有个坑,即:jpa的mongodb为了方便将document还原成java对象,会在写入的时候设置一个字段”_class”,而Example在查询时候回将EmployeeInfoDO 的classname作为查询条件,而我们写入DB的时候在注入MappingMongoConverter已经忽略调了_class字段,所以当然就查不到数据了。

MappingMongoConverter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, BeanFactory beanFactory) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);

MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
try {
mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
} catch (NoSuchBeanDefinitionException ignore) {
}

// Don't save _class to mongo
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));

return mappingConverter;
}

解决方案

设置有效的ExampleMatcher忽略”_class”

1
public static final ExampleMatcher EXAMPLE_MATCHER_IGNORE_CLASS = ExampleMatcher.matchingAll().withIgnorePaths("_class");

Go语言-painc和recover技巧

Posted on 2020-07-06 | Edited on 2022-09-21 | In golang , 编程技巧

Java程序为了防止意外退出,在处理异常时候有try-catch,golang我们则会想到recover函数来处理painc。

recover的使用

recover()是go的内置函数,只能放在defer中,recover的返回值是painc中的对象。常见的用法如下:

1
2
3
4
5
6
7
8
9
func process(i int){
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到的错误:%s\n", r)
}
}()

painc("退出")
}

注意事项

  1. recover一定要在defer中,且不能直接调用 比如 defer recover 。
  2. 如果循环体中出现了painc,因为recover在defer中,则循环会被break,并且recover会在跳出循环后执行。见下面代码
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
//我们希望的是遇到偶数painc之后recover捕获error,还能继续执行但是循环已经被跳出了
func Recovery() {
for i := 0; i < 10; i++ {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到的错误:%s\n", r)
}
}()

if i%2 == 0 {
panic(fmt.Sprintf("偶数退出:%v",i))
}else{
fmt.Println(i)
}
}
}

//正确做法
func Recovery() {
for i := 0; i < 10; i++ {
process(i)
}
}

func process(i int){
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到的错误:%s\n", r)
}
}()

if i%2 == 0 {
panic(fmt.Sprintf("偶数退出:%v",i))
}else{
fmt.Println(i)
}
}

Go语言-技巧set类实现

Posted on 2020-07-01 | Edited on 2022-09-21 | In golang , 编程技巧

利用slice和map实现了个非线程安全的LinkedSet,map的value是元素的index。也支持排序。代码如下:

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
package collections

import (
"encoding/json"
"fmt"
"sort"
)

// Set LinkedHashSet 支持数组排序
type Set struct {
m map[interface{}]int //元素和索引映射
list []interface{} //元素数组
compare func(i, j interface{}) bool //排序函数
}

// NewSet 构造方法
func NewSet() *Set {
s := new(Set)
s.initEle()
return s
}

// initEle 初始化数组元素
func (set *Set) initEle() {
set.m = make(map[interface{}]int)
set.list = make([]interface{}, 0)
}

// SortAble 设置排序依据
// compare--用于排序
func (set *Set) SortAble(compare func(i, j interface{}) bool) *Set {
set.compare = compare
return set
}

// Add 添加元素
func (set *Set) Add(keys ...interface{}) *Set {
//set.Lock()
//defer set.Unlock()
for _, key := range keys {
if _, ok := set.m[key]; !ok {
set.list = append(set.list, key)
set.m[key] = len(set.list) - 1
}
}
return set
}

// Delete 删除元素
func (set *Set) Delete(keys ...interface{}) {
//set.Lock()
//defer set.Unlock()
for _, key := range keys {
if v, ok := set.m[key]; !ok {
if len(set.list)-1 < v {
set.list = append(set.list[0:v], set.list[v+1:]...)
}
delete(set.m, key)
}
}
}

// Get 获取元素
func (set *Set) Get(key interface{}) interface{} {
//set.RLock()
//defer set.RUnlock()
if v, ok := set.m[key]; ok {
return set.list[v]
}
return nil
}

// Contains 获取元素
func (set *Set) Contains(key interface{}) bool {
//set.RLock()
//defer set.RUnlock()
_, ok := set.m[key]
return ok
}

// IndexOf 获取元素下标
func (set *Set) IndexOf(key interface{}) int {
//set.RLock()
//defer set.RUnlock()
if i, ok := set.m[key]; ok {
return i
}
return -1
}

// Any 多个元素有人一个true 则为true
func (set *Set) Any(cond func(key interface{}) bool) bool {
//set.RLock()
//defer set.RUnlock()
for _, key := range set.list {
if cond(key) {
return true
}
}
return false
}

// Diff 查看俩个set不同的元素,返回新的set
func (set *Set) Diff(dest *Set) *Set {
result := NewSet()
set.Foreach(func(ele interface{}) {
if dest == nil || dest.Size() == 0 || !dest.Contains(ele) {
result.Add(ele)
}
})

if dest != nil && dest.Size() > 0 {
dest.Foreach(func(ele interface{}) {
if !set.Contains(ele) {
result.Add(ele)
}
})
}
return result
}

// Difference 取s和dest差集,返回新的set
func (set *Set) Difference(dest *Set) *Set {
result := NewSet()
set.Foreach(func(ele interface{}) {
if dest == nil || dest.Size() == 0 || !dest.Contains(ele) {
result.Add(ele)
}
})
return result
}

// Union 取交集,返回新的set
func (set *Set) Union(dest *Set) *Set {
result := NewSet()
set.Foreach(func(ele interface{}) {
if dest != nil && dest.Size() > 0 && dest.Contains(ele) {
result.Add(ele)
}
})
return result
}

// Size 返回set的size
func (set *Set) Size() int {
//set.RLock()
//defer set.RUnlock()
return len(set.list)
}

// Elements 返回element列表
func (set *Set) Elements() []interface{} {
//set.RLock()
//defer set.RUnlock()
return set.list
}

// SetIterateFunc 遍历的handler
type SetIterateFunc func(interface{})

// Foreach 遍历
func (set *Set) Foreach(foreach SetIterateFunc) {
//set.RLock()
//defer set.RUnlock()
if len(set.list) == 0 {
return
}
for _, item := range set.list {
foreach(item)
}
}

// Sort 排序
func (set *Set) Sort() {
//set.Lock()
//defer set.Unlock()
sort.Sort(set)
}

func (set *Set) Swap(i, j int) {
set.list[i], set.list[j] = set.list[j], set.list[i]
set.m[set.list[i]] = i
set.m[set.list[j]] = j
}
func (set *Set) Len() int { return len(set.list) }
func (set *Set) Less(i, j int) bool {
if set.compare == nil {
return false
}
return set.compare(set.list[i], set.list[j])
}

// SetIsEmpty 判断set是否为空
func SetIsEmpty(set *Set) bool {
return set == nil || set.Size() == 0
}

// MarshalJSON 实现json.Marshaler接口
func (set *Set) MarshalJSON() ([]byte, error) {
return json.Marshal(set.list)
}

// UnmarshalJSON 实现json.Unmarshaler接口
func (set *Set) UnmarshalJSON(b []byte) error {
set.initEle()
var ele []interface{}
err := json.Unmarshal(b, &ele)
if err != nil {
return err
}
set.Add(ele...)
return err
}

//////////////////////////////////////////
func (set Set) String() string {
return fmt.Sprintf("%v", set.list)
}

排序方法的使用方法

1
2
3
4
5
s:=set.NewSet().Add(....).AddOrderBy(OrderByHandlerFunc(i,j interface{})bool{
//排序逻辑
return true
})
sort(s)

Go语言-技巧之接口函数

Posted on 2020-07-01 | Edited on 2022-09-21 | In golang , 编程技巧

基本定义

定义:接口函数顾名思义,他本身是一个函数,但是实现了一个接口,在这个接口中自己调用自己。
使用场景:适用于接口里只有一个函数的场景
作用:面向接口编程有时候我们为了适配不通的场景,往往需要实现多个接口,这样就会凭空出现很多接口的实现者。如果我们用接口函数就可以很好的规避这一点。
命名规范:在接口名称后面加上Func,比如接口是KeyHandler,接口函数就是KeyHandlerFunc

Show Code

我最早接触的接口函数就是http.handlerFunc,它实现了接口Handler,见下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

我们更多的将它作为接口的拦截器用。这时候会有俩种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

//第一种. 采用了接口函数,我们只需要实现一个func(w http.ResponseWriter, r *http.Request),并且强转成http.HandlerFunc
func AuthInterceptor(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//...before
next.ServeHTTP(w, r)//业务逻辑
//...after
})
}

//第二种. 不采用接口函数,这时候你要显示的定义AuthLogic实现http.Handler接口,我们程序中会多出来很多AuthLogic这种结构体
func AuthInterceptor(next http.Handler) http.Handler {
return AuthLogic{next:next}
}

type AuthLogic struct {
next http.Handler
}

func (a AuthLogic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//...before
a.next.ServeHTTP(w, r)
//...after
}

所以函数接口让我们的接口调用更加的简便灵活。

jvm启动参数NewRatio不生效

Posted on 2020-06-28 | Edited on 2022-09-21 | In 经验积累 , 项目积累

现象:jvm启动参数的配置,由于-Xmn的值优先级更高,所以NewRatio会被-Xmn覆盖。见下面的测试:

1
2
3
4
5
6
7
8
9
# 这里NewSize是20MB
java -Xmn20M -XX:NewRatio=2 -Xms50M -XX:+PrintFlagsFinal -version | grep -E '(Old|New)Size'
uintx MaxNewSize := 20971520 {product}
uintx NewSize := 20971520 {product}
uintx NewSizeThreadIncrease = 5320 {pd product}
uintx OldSize := 31457280 {product}
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode) {product}
1
2
3
4
5
6
7
8
9
#这里是50MB的1/3,可见NewRatio生效了
java -XX:NewRatio=2 -Xms50M -XX:+PrintFlagsFinal -version | grep -E '(Old|New)Size'
uintx MaxNewSize := 1073741824 {product}
uintx NewSize := 17301504 {product}
uintx NewSizeThreadIncrease = 5320 {pd product}
uintx OldSize := 35127296 {product}
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)

共享服务中心的建设之路

Posted on 2020-06-24 | Edited on 2022-09-21 | In 读后感 , 企业IT架构转型之道

Why && When 为什么什么时候要建立共享服务中心

业务发展到一定阶段,之前的服务都散落在各自的业务领域,那么对于使用者来说这种野蛮发展带来了如下的问题:

  1. 服务数量多业务范围涵盖越来越广,如何快速的找到服务成为了要解决的问题
  2. 应用和架构越分越细,服务越来越专业化,学习成本变高
  3. 服务缺乏缺乏统一的管控,往往会出现不知道这些服务又哪些下游,是如何调用,自己随意发布版本导致服务不稳定;
    1. 防止恶意调用
    2. 防止认为的错误调用
    3. 管理服务的依赖关系,提供统一的SLA协议规范
  4. 缺少治理层:之前服务的标准,接入方式,鉴权方式散落在各个角落(wiki、文档、开发人员的大脑),需要一个平台对服务进行收敛,减少接入成本;

What 共享服务中心(SPAS)是做什么用的职责是什么

它的主要职责是将一个个的服务封装成服务组件为服务消费者提供服务的平台,为服务提供者提供统一的服务治理,为服务的消费者降低接入成本,提供更稳定的服务

建设思路:

  1. 找到服务化的对象
  2. 提供服务化基础设施
  3. 服务下沉为共享服务中心的组件(实施 由服务提供者接入)
  4. 增强服务和基础设施实现服务治理(提供服务鉴权、运营报表、运维工具等)

阿里巴巴的实现思路

  1. 服务化对象是API:面向API,将API封装成服务(通用)
  2. 提供服务化基础设施【有借鉴意义】
    1. 服务描述的元信息
    2. 服务注册发现机制(该服务有一个统一的服务发现平台,比如mq的订阅管控关系, hsf的注册中心等)
    3. 服务的访问控制和安全策略
    4. 应用服务的Protal
    5. 服务的依赖管理和运维管理
    6. 服务的SLA协议
    7. 消费者管理工具与支持工具
    8. 服务化辅助工具支持
    9. 服务化的报表与分析工具
  3. 服务化实施阶段:
    1. API as Service
    2. Product as Service
    3. Solution as Service

How 如何协作

  • 共享服务中心:提供服务的治理能力以及相关基础设施,指导服务的接入
  • 服务提供者:服务将服务接入到共享中心,管理服务,提供场景化解决方案
  • 服务消费者:接入服务,按照规范使用服务,并且给于服务反馈

共享服务中心和前端(前端事业部)

  1. 和前端核心业务简历紧密沟通机制,积极参加这些部门的技术需求会议,及时响应
  2. 分歧上升机制:遇到分歧,采取层层上升机制
  3. 轮岗制度:保证换位思考
  4. 共享服务中心的发展方式,当发现一些服务存在于不同的前端,可以尝试集成、沉淀到共享服务中心
    1. 共享服务中心的架构师或者RD有独自沉淀能力,和业务方沟通自己实施;
    2. 共享服务中心的架构师暂时没有能力,采用共建的方式,业务方和共享服务中心的人员一起实施保障快速落地,同时也能在参与工程中培养这方面的技术能力。

企业的稳定性建设

Posted on 2020-06-23 | Edited on 2022-09-21 | In 读后感 , 企业IT架构转型之道

限流和降级

见稳定性看法

接入层可以在nginx层做,服务层阿里采用的sentinal,相比开源出来的jar包,它是个集成了hsf,集策略配置,下发于一体的限流、降级管控平台。

流量调度平台

有些系统由于发布、网络抖动、故障等原因会导致服务中某个节点出现负载升高,导致高节点响应时间变长,请求排队等情况,进一步可能威胁到上下游的稳定性。

相比于上面提到的限流和降级,这种流量调度平台更专注解决服务某一个节点的问题。

实现原理

  1. 采集系统+业务指标
  2. 通过一定时间维度的指标和故障模型匹配
  3. 后续故障处置:降权(hsf的服务权重)、摘除(踢掉节点)。

开关服务

感觉很像spring-cloud全家桶中的config服务的作用,用来下发开关

容量压测和容量评估

  1. 线上流量引入,测试单机的qps,得到单机服务的性能基线
  2. 根据单机qps来评估集群容量,在双11大促等场景对服务承载能力掌控更精准,节省服务器资源

全链路压测

!很重要,也是阿里的法宝之一,方法论如下:

  1. 基础数据抽取:对现有的生产数据进行筛选、过滤、抽取
  2. 链路模型构造:构造压测链路的模型
  3. 链路验证:对模型进行验证
  4. 业务改造:业务对于压测的幂等支持,放线上数据污染能
  5. 数据平台:全链路压测的数据极地
  6. 流量平台:操控中心(对压测进行限制,配置),压测引擎(部署在外网cdn中)
  7. 中间件支持:压测标志支持,数据隔离跟踪等。
  8. 影子表:通过设置影子表将压测数据录入到影子表方变后续管理
  9. 安全策略支持:对压测数据的识别,不要误认为攻击,对于压测数据的监控防止恶意模仿

业务数据一致性

阿里内部将BCP(bussiness check platform),为了不影响业务,是通过时间触发的原理实现。

即。感知数据的变化(精卫、mq)之后根据配置的事件类型找到检测模型,进行规则校验,然后结果入库,发现异常发送报警通知。

服务管控-分布式服务全链路跟踪和分析系统

Posted on 2020-06-22 | Edited on 2022-09-21 | In 读后感 , 企业IT架构转型之道

系统服务化或者后续的微服务架构,做到了服务隔离,业务解耦,在带来这些便利的同时也回答带来了排查错误不好定位的问题,因为一个请求往往横跨多个服务。还有异步化等等。
所以一套好用的满足分布式系统的全链路跟踪分析系统可以说是整个服务化最重要的基础组件。

实现方式

俩部分,日志采集,和日志分析

日志采集

  • 采集:业务几乎无感,业务服务只需要向自身所在的服务器写本地log,服务器有一个采集日志的agent,根据采集中心的配置去制定的目录下实时同步log给日志处理中心。
  • 处理:通过storm spark flink将日志进行清洗,实时的我们可以打到hbase和es中。离线的我们可以存到hadoop中
  • 日志格式:全局的TraceID(本次调用的全局ID)+RpcID(本次调用中该节点的ID,建议是有顺序的支持嵌套调用比如1,1.1,1.1.1)+时间戳+数据标签(分类)+业务组件+msg
  • 采集管控:业务高峰可能产生很多log我们对于一段时间内相同的log要进行降频控制防止对业务有影响

常用组件

  • agent开发(java、go)
  • 数据通道kafka,rocktmq
  • 日志清洗spark,flink,strom
  • 数据存储es,hbase,hadoop

具体应用场景

  • 监控业务:原始的监控服务器指标已经不满足服务化的需求了,因为有可能服务指标良好,但是服务已经不可用了,我们可以通过这台全链路的跟踪系统通过自己埋点构建有个性的监控系统
  • 实时全链路排查:加强业务的管控,实时的了解业务调用链路的异常出在哪里,快速排错,进行有针对性的优化
  • 离线的全链路分析:针对业务架构师,可以分析一段时间内接口调用情况,查看一个接口整体的设计。比如:依赖是否和合理,强依赖是否平均耗时较高,若依赖是否可以异步化等,使我们服务的编排更合理,架构更加贴近业务。
1…345…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0