开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring Boot如何利用AOP巧妙记录操作日志? 本篇

发表于 2021-01-22

本篇要点

  • 简要回顾SpringAOP的相关知识点:关键术语,通知类型,切入点表达式等等。
  • 介绍SpringBoot快速启动测试AOP,巧妙打印日志信息。

简单回顾SpringAOP的相关知识点

SpringAOP的相关的知识点包括源码解析,为了加深印象,这边再做一个简短的回顾:

1、AOP关键术语

Spring Boot如何利用AOP巧妙记录操作日志?

  • 切面(Aspect):也就是我们定义的专注于提供辅助功能的模块,比如安全管理,日志信息等。
  • 连接点(JoinPoint):切面代码可以通过连接点切入到正常业务之中,图中每个方法的每个点都是连接点。
  • 切入点(PointCut):一个切面不需要通知所有的连接点,而在连接点的基础之上增加切入的规则,选择需要增强的点,最终真正通知的点就是切入点。
  • 通知方法(Advice):就是切面需要执行的工作,主要有五种通知:before,after,afterReturning,afterThrowing,around。
  • 织入(Weaving):将切面应用到目标对象并创建代理对象的过程,SpringAOP选择再目标对象的运行期动态创建代理对
  • 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加方法或字段。

2、通知的五种类型

  • 前置通知Before:目标方法调用之前执行的通知。
  • 后置通知After:目标方法完成之后,无论如何都会执行的通知。
  • 返回通知AfterReturning:目标方法成功之后调用的通知。
  • 异常通知AfterThrowing:目标方法抛出异常之后调用的通知。
  • 环绕通知Around:可以看作前面四种通知的综合。

3、切入点表达式

上面提到:连接点增加切入规则就相当于定义了切入点,当然切入点表达式分为很多种,这里主要学习execution和annotation表达式。

execution

  • 写法:execution(访问修饰符 返回值 包名.包名……类名.方法名(参数列表))
  • 例:execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())
  • 访问修饰符可以省略,返回值可以使用通配符*匹配。
  • 包名也可以使用*匹配,数量代表包的层级,当前包可以使用..标识,例如* *..AccountServiceImpl.saveAccount()
  • 类名和方法名也都可以使用*匹配:* *..*.*()
  • 参数列表使用..可以标识有无参数均可,且参数可为任意类型。

全通配写法:* *…*.*(…)

通常情况下,切入点应当设置在业务层实现类下的所有方法:* com.smday.service.impl.*.*(..)。

@annotation

匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。

@annotation(com.hyh.annotation.Log):指定Log注解方法的连接点。

4、AOP应用场景

  • 记录日志
  • 监控性能
  • 权限控制
  • 事务管理

快速开始

引入依赖

如果你使用的是SpringBoot,那么只需要引入:spring-boot-starter-aop,框架已经将spring-aop和aspectjweaver整合进去。

1
2
3
4
xml复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义日志信息封装

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
typescript复制代码/**
* Controller层的日志封装
* @author Summerday
*/
@Data
@ToString
public class WebLog implements Serializable {

private static final long serialVersionUID = 1L;

// 操作描述
private String description;

// 操作时间
private Long startTime;

// 消耗时间
private Integer timeCost;

// URL
private String url;

// URI
private String uri;

// 请求类型
private String httpMethod;

// IP地址
private String ipAddress;

// 请求参数
private Object params;

// 请求返回的结果
private Object result;

// 操作类型
private String methodType;
}

自定义注解@Log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

/**
* 描述
*/
String description() default "";

/**
* 方法类型 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}

定义测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@RestController
public class HelloController {

@PostMapping("/hello")
@Log(description = "hello post",methodType = MethodType.INSERT)
public String hello(@RequestBody User user) {
return "hello";
}

@GetMapping("/hello")
@Log(description = "hello get")
public String hello(@RequestParam("name") String username, String hobby) {
int a = 1 / 0;
return "hello";
}
}

定义切面Aspect与切点Pointcut

用@Aspect注解标注标识切面,用@PointCut定义切点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 定义切面
* @author Summerday
*/

@Aspect
@Component
public class LogAspect {

private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

/**
* web层切点
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web层的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log注解标注的方法
*/
@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}
}

定义通知方法Advice

这里使用环绕通知,

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
scss复制代码/**
* 定义切面
* @author Summerday
*/

@Aspect
@Component
public class LogAspect {

private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

/**
* web层切点
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web层的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log注解标注的方法
*/

@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}


/**
* 环绕通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//获取请求对象
HttpServletRequest request = getRequest();
WebLog webLog = new WebLog();
Object result = null;
try {
log.info("=================前置通知=====================");
long start = System.currentTimeMillis();
result = joinPoint.proceed();
log.info("=================返回通知=====================");
long timeCost = System.currentTimeMillis() - start;
// 获取Log注解
Log logAnnotation = getAnnotation(joinPoint);
// 封装webLog对象
webLog.setMethodType(logAnnotation.methodType().name());
webLog.setDescription(logAnnotation.description());
webLog.setTimeCost((int) timeCost);
webLog.setStartTime(start);
webLog.setIpAddress(request.getRemoteAddr());
webLog.setHttpMethod(request.getMethod());
webLog.setParams(getParams(joinPoint));
webLog.setResult(result);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
log.info("{}", JSONUtil.parse(webLog));
} catch (Throwable e) {
log.info("==================异常通知=====================");
log.error(e.getMessage());
throw new Throwable(e);
}finally {
log.info("=================后置通知=====================");
}
return result;
}

/**
* 获取方法上的注解
*/
private Log getAnnotation(ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
return method.getAnnotation(Log.class);
}


/**
* 获取参数 params:{"name":"天乔巴夏"}
*/
private Object getParams(ProceedingJoinPoint joinPoint) {
// 参数名
String[] paramNames = getMethodSignature(joinPoint).getParameterNames();
// 参数值
Object[] paramValues = joinPoint.getArgs();
// 存储参数
Map<String, Object> params = new LinkedHashMap<>();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
// MultipartFile对象以文件名作为参数值
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
value = file.getOriginalFilename();
}
params.put(paramNames[i], value);
}
return params;
}

private MethodSignature getMethodSignature(ProceedingJoinPoint joinPoint) {
return (MethodSignature) joinPoint.getSignature();
}


private HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest();
}

}

这里处理webLog的方式有很多种,考虑性能,可以采用异步方式存入数据库,相应代码已经上传至Gitee。

测试

1
2
3
4
bash复制代码POST http://localhost:8081/hello
Content-Type: application/json

{ "id" : 1, "username" : "天乔巴夏", "age": 18 }

结果如下:

1
2
3
4
json复制代码=================前置通知=====================
=================返回通知=====================
{"ipAddress":"127.0.0.1","description":"hello post","httpMethod":"POST","params":{"user":{"id":1,"age":18,"username":"天乔巴夏"}},"uri":"/hello","url":"http://localhost:8081/hello","result":"hello","methodType":"INSERT","startTime":1605596028383,"timeCost":28}
=================后置通知=====================

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节跳动高频编程题—模拟微信发红包 初步想法-暴力版本 暴力

发表于 2021-01-22

模拟微信发红包,n个人抢总金额为m的红包,请设计一个算法并实现

这个题目是我在上周二面试字节跳动时候遇到的,当时写出来的还是一个暴力版本,回来之后就和朋友交流了一下,很多人也遇到过了,所以这个题目算是字节跳动研发/测试/测开系列经常出的题目,还挺有意思的,记录分享一下

初步想法-暴力版本

说实话,刚开始看到这个题目的时候,我的想法是这样的:

  • 每次在(0, m)这个区间内随机一个值,记为r;
  • 计算一下剩余金额m-r,剩余金额m-r必须大于(n-1)*0.01,不然后面的n-1个人无法完成分配;
  • 按照顺序随机n-1次,最后剩下的金额可以直接当做最后一个红包,不需要随机;

嗯,听上去不错,然后动手实现了一下这个解法版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码def money_alloc(m, n):
if n * 0.01 > m:
raise ValueError("not enough money or too many people")
result = []
m = round(m, 2)
while n > 1:
# 这里需要注意两个细节:
# - random.uniform(a, b)的随机区间是≥a&≤b,即[a, b]
# - random.uniform(a, b)随机出来的值是0.0012032010230123,保留两位小数之后是有可能出现等于0.00的情况
alloc_result = round(random.uniform(0.01, m-0.01), 2)
# (m - alloc_result) < (n * 0.01)的判断是为了保证这一次的随机之后,后续的总金额可以继续分配,否则将重新随机指导满足这个条件
if (m - alloc_result) < (n * 0.01) or alloc_result <= 0.00:
continue
result.append(alloc_result)
n = n - 1
m = m - alloc_result

result.append(round(m, 2))
return result

看上去OK的,接下来我用相对正常的数据自测了一下,类似这样:

1
2
python复制代码for _ in xrange(10):
print money_alloc(10, 5)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
python复制代码[3.73, 6.15, 0.06, 0.03, 0.03]
[4.28, 0.8, 1.09, 2.13, 1.7]
[0.66, 2.27, 5.5, 1.5, 0.07]
[6.55, 1.46, 0.82, 0.2, 0.97]
[5.48, 0.47, 0.65, 0.48, 2.92]
[6.4, 3.09, 0.29, 0.01, 0.21]
[9.94, 0.02, 0.01, 0.01, 0.02]
[4.98, 4.97, 0.01, 0.01, 0.03]
[8.17, 1.3, 0.18, 0.17, 0.18]
[3.49, 5.45, 0.36, 0.3, 0.4]

从这个随机结果里面,我们发现了这个解法的一个特点,就是红包金额越来越小,等于说:谁先抢,谁能抢到的红包金额就越大。

接着,我们用相对极限的情况(比如1块钱 ,100个人分)再次测试的时候,悲剧发生了,程序陷入了深深的随机当中无法自拔,究其原因在于越往后,金额的可用区间就越小,随机的压力就越大。

总结一下这个暴力解法:

  • 大众思路,适合钱多、人少的场景,在钱少、人多的情况下会陷入随机死循环;
  • 公平性太差,先抢的优势过大,显然不符合当前微信红包的这种公平性;

暴力版本二

既然钱少、人多的情况下会陷入随机死循环,那么是不是就无解了呢,当然不是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码def money_alloc(m, n):
if n * 0.01 > m:
raise ValueError("not enough money or too many people")
result = []
m = round(m, 2)
# 加入随机次数统计
random_count = 0
while n > 1:
alloc_result = round(random.uniform(0.01, m-0.01), 2)
if (m - alloc_result) < (n * 0.01) or alloc_result <= 0.00:
random_count += 1
# 随机10次还没有结果,直接给丫来一个0.01,行不行?
if random_count > 10:
alloc_result = 0.01
random_count = 0
result.append(alloc_result)
n = n - 1
m = m - alloc_result
continue
result.append(alloc_result)
n = n - 1
m = m - alloc_result
result.append(round(m, 2))
return result

这里暴力版本二里面,主要加入了一个随机次数统计值random_count,来避免随机陷入“死循环”,代码逻辑比较简单,就不赘述了

接着我们再次对这个算法进行测试,如下:

1
2
python复制代码for _ in xrange(10):
print money_alloc(1, random.randint(10, 99))

测试结果如下:

1
2
3
4
5
python复制代码[0.03, 0.13, 0.16, 0.01, 0.1, 0.2, 0.06, 0.02, 0.01, 0.01, 0.01, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.03]

[0.79, 0.02, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.03]

[0.01, 0.08, 0.01, 0.01, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.03]

OK,感觉还凑合,暴力版本二虽然解决了暴力版本一当中的死循环问题,但是公平性问题还是没有被解决。

接下来介绍一下另外的两种解法:二倍均值法和线段切割法,这两种方法借鉴了小灰的算法思路。

二倍均值法

在暴力版本中,不公平的问题主要体现在前面的区间太大,后面可用的随机区间太小,导致了红包金额严重失衡的问题,所以二倍均值法的核心在于稳定随机的区间。

先介绍一下二倍均值的思路:

  • 计算一个平均值,比如10块钱,10个人,人均可得1块钱;
  • 第一次随机时,将随机区间定义在(0, 2)之间,随机得到一个值r1,即第一个红包;
  • 接着进行第二次随机,计算剩余金额10-r1,计算剩余人均(10-r1)/9,然后在[0, 人均 * 2]中随机出第二个红包;
  • 以此类推,完成红包分配的过程;

我当初看到这个思路的时候,有这样的一个疑问:

为什么要将均值*2呢,直接在(0, 均值)这个区间进行随机不行吗?比如说10人分10块钱,第一次为什么不直接在(0, 1)这个区间而要再(0, 2)这个区间呢?

关于随机的问题,有点超纲了,总结来说就是在(a, b)这区间内的随机,那么随机出来的值在(a+b)/2附近的概率更大

按照这个思路继续分析下去,对于上面这个实例来说,基本上可以让每个人抢到的红包都在1块钱左右

我们用Python实现一下看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def money_alloc(m, n):
if n * 0.01 > m:
raise ValueError("not enough money or too many people")
result = []
m = round(m, 2)
while n > 1:
avg_money = round(m / n * 2, 2) - 0.01
alloc_result = round(random.uniform(0.01, avg_money), 2)
result.append(alloc_result)
n = n - 1
m = m - alloc_result
result.append(round(m, 2))
return result

接着用正常测试用例,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码# 10块钱5个人分
for _ in xrange(10):
print money_alloc(10, 5)

# 分配结果, 随机结果在2附近的值一眼看下去还是居多的
[1.83, 0.78, 0.28, 2.74, 4.37]
[1.17, 4.13, 0.54, 0.66, 3.5]
[1.37, 1.67, 1.3, 5.57, 0.09]
[3.49, 2.5, 1.22, 0.75, 2.04]
[2.1, 3.2, 0.5, 3.19, 1.01]
[2.83, 2.01, 2.12, 1.2, 1.84]
[2.97, 0.79, 1.45, 1.52, 3.27]
[2.77, 1.64, 1.53, 0.41, 3.65]
[3.49, 0.88, 0.39, 3.26, 1.98]
[1.79, 3.61, 2.55, 1.21, 0.84]

接着用极限测试用例,测试一下:

1
2
3
bash复制代码# 1块钱50个人分,分配结果(数据太多,随机取了两个)
[0.01, 0.03, 0.01, 0.01, 0.01, 0.02, 0.03, 0.02, 0.03, 0.03, 0.01, 0.01, 0.01, 0.02, 0.02, 0.02, 0.01, 0.02, 0.02, 0.02, 0.02, 0.02, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.02, 0.02, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.03, 0.02, 0.01, 0.02, 0.03, 0.02, 0.01, 0.01, 0.02, 0.04, 0.03, 0.04, 0.01, 0.02]
[0.03, 0.03, 0.02, 0.02, 0.03, 0.03, 0.03, 0.02, 0.02, 0.01, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.02, 0.01, 0.02, 0.03, 0.02, 0.01, 0.03, 0.02, 0.02, 0.03, 0.01, 0.02, 0.03, 0.02, 0.02, 0.01, 0.01, 0.03, 0.02, 0.01, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.02, 0.02, 0.02, 0.01, 0.02, 0.01, 0.01, 0.02]

二倍均值法很好的解决了暴力版本当中的公平性问题,让每个人能够抢到的红包差距不会太大

总结一下二倍均值法:

  • 解决了暴力版本中的公平性问题,但实际的微信红包在分配结果上并不是均等的,具体大家应该都有体会

线段切割法

为了让最终的分配结果体现出差异性,更贴近实际使用中的微信抢红包过程,可以考虑线段切割法。

线段切割法的思路大致如下:

1、将红包的分配过程想象成线段切割,红包的总金额为线段的总长度;

2、在线段上标记处N-1个不重复的点,线段就被切割成了N分长度(金额)不同的小线段;

3、标记方法:每次都在(0, m)这个区间随机出一个值,即为标记点;

4、最后计算相邻标记点之间的差距(子线段长度)即为红包金额;

话不多说,直接上Python实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码def money_alloc(m, n):
if n * 0.01 > m:
raise ValueError("not enough money")
# 0为线段的起点
result = [0]
m = round(m, 2)
while n > 1:
alloc_result = round(random.uniform(0.01, m - 0.01), 2)
if alloc_result in result:
continue
result.append(alloc_result)
n = n - 1
# m为线段的终点
result.append(m)
result.sort()
return [round(result[index+1]- item, 2) for index, item in enumerate(result) if index < len(result) - 1]

测试一下:

1
2
3
python复制代码[1.07, 6.08, 2.85]

[0.04, 0.11, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.07, 0.02, 0.02, 0.05, 0.12, 0.02, 0.01, 0.01, 0.01, 0.13, 0.02, 0.01, 0.05, 0.03, 0.02, 0.07, 0.01, 0.02, 0.02, 0.01, 0.03, 0.01]

OK,到这里似乎所有的问题都已经完美解决了,这个解法看起来好完美。

But…事实真的是这样吗?

现在,我们抛开实际的场景,回归到这个算法本身,不妨测试一下1万块3万人分,测试代码如下:

1
2
3
4
5
python复制代码for _ in xrange(5):
a = time.time()
money_alloc(10000, 30000)
b = time.time()
print b - a

测试结果大概如下:

1
2
3
4
5
python复制代码7.04587507248
7.84848403931
7.50485801697
7.98592209816
8.28649902344

在我的电脑上,大概需要耗时7、8秒的样子,这…

不慌不慌,我们先分析一下代码可能的问题:

  1. 随机3W+次,这个过程本省确实耗时,但感觉也没有什么改善空间了;
  2. alloc_result in result在result很大的时候,查找效率太低,非常耗时,这个必须改掉;
  3. result.append(alloc_result)如果是有序插入,那么后续的list.sort就没必要了;
  4. 另外,list.sort()耗时么?

什么数据结构的查找效率最高呢?当然是hashmap,也就是dict啦,并且在测试过程中发现list.sort()耗时基本在10ms以内,优化空间不大,所以没有考虑有序插入。

线段切割法-优化版本

最终优化之后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码def money_alloc(m, n):
if n * 0.01 > m:
raise ValueError("not enough money")
result = [0]
# 牺牲一部分空间,提升查重的效率
tmp_dict = dict()
m = round(m, 2)
while n > 1:
alloc_result = round(random.uniform(0.01, m - 0.01), 2)
# hash 版本
if alloc_result in tmp_dict:
continue
tmp_dict[alloc_result] = None
result.append(alloc_result)
n = n - 1

result.append(m)
result.sort()
return [round(result[index+1]- item, 2) for index, item in enumerate(result) if index < len(result) - 1]

优化之后,我们用刚才的测试代码再次测试一下看看效果:

1
2
3
4
5
python复制代码0.197105169296
0.169443130493
0.162744998932
0.167745113373
0.147526979446

7秒到200ms的效率提升,不要太直观

至于空间复杂度,留给小伙伴本自己研究吧,希望有更好的版本可以学习一下

问题总结

  • 抢红包算法的三种解法:暴力分配、二倍均值、线段切割;
  • 暴力分配仅仅只能适用于这个问题本身,实际过程中没有任何应用价值;
  • 二倍均值解决了暴力分配的问题,但缺少了大红包的惊喜;
  • 在实际的微信红包分配过程中,线段切割优化和不优化实际上差异应该不至于太大,但是追求性能,优化版本还是有更多的改进空间;

附录:关于random.uniform

uniform一般用于生成浮点数,关于random.uniform的功能描述:

1
2
python复制代码|  uniform(self, a, b)
| Get a random number in the range [a, b) or [a, b] depending on rounding.

现在看来这个描述并不准确

实际运用时,我一直认为这个随机区间是[a, b],实则不然

1
2
3
4
5
6
7
8
9
10
python复制代码>>> random.uniform(0, 1)
0.15407896775722285
>>> random.uniform(0, 1)
0.16189270320003113
>>> random.uniform(0, 0) # a == b
0.0
>>> random.uniform(0, -1) # a > b
-0.8838459569306347
>>> random.uniform(0, -100) # a > b
-76.93918157758513

所以其实a>b的时候也是可以进行浮点数随机的,随机区间并不是绝对意义的最小值是a,最大值是b

你, 学到了吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里一面:如何保证API接口数据安全? 前言 接口签名 签名

发表于 2021-01-22

前言

前后端分离的开发方式,我们以接口为标准来进行推动,定义好接口,各自开发自己的功能,最后进行联调整合。无论是开发原生的APP还是webapp还是PC端的软件,只要是前后端分离的模式,就避免不了调用后端提供的接口来进行业务交互。

网页或者app,只要抓下包就可以清楚的知道这个请求获取到的数据,也可以伪造请求去获取或攻击服务器;也对爬虫工程师来说是一种福音,要抓你的数据简直轻而易举。那我们怎么去解决这些问题呢?

接口签名

我们先考虑一下接口数据被伪造,以及接口被重复调用的问题,要解决这个问题我们就要用到接口签名的方案,

签名流程

图片

签名规则

1、线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret

2、加入timestamp(时间戳),5分钟内数据有效

3、加入临时流水号 nonce**(防止重复提交)**,至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。

4、加入签名字段signature,所有数据的签名信息。

以上字段放在请求头中。

签名的生成

签名signature字段生成规则

所有动态参数 = 请求头部分 + 请求URL地址 + 请求Request参数 + 请求Body

上面的动态参数以key-value的格式存储,并以key值正序排序,进行拼接

最后拼接的字符串 在拼接appSecret

signature = DigestUtils.md5DigestAsHex**(sortParamsMap +** appSecret**)**

即拼接成一个字符串,然后做md5不可逆加密

请求头部分

请求头=“appId=xxxx&nonce=xxxx×tamp=xxxx&sign=xxx”

请求头中的4个参数是必须要传的,否则直接报异常

请求URL地址

这个就是请求接口的地址包含协议,如

mso.xxxx.com.cn/api/user

请求Request参数

即请求为Get方式的时候,获取的传入的参数

请求Body

即请求为Post时,请求体Body

从request inputstream中获取保存为String形式

签名算法实现

**基本原理其实也比较简单,就是自定义filter,对每个请求进行处理;整体流程如下:
**

  1. 验证必须的头部参数
  2. 获取头部参数,request参数,Url请求路径,请求体Body,把这些值放入SortMap中进行排序
  3. 对SortMap里面的值进行拼接
  4. 对拼接的值进行加密,生成sign
  5. 把生成的sign和前端传入的sign进行比较,如果不相同就返回错误

我们来看一下代码

1
2
3
4
5
java复制代码@Component
public class SignAuthFilter extends OncePerRequestFilter{
static final String FAVICON = "/favicon.ico";
static final String PREFIX = "attack:signature:";
}

图片

以上是filter类,其中有个appSecret需要自己业务去获取,它的作用主要是区分不同客户端app。并且利用获取到的appSecret参与到sign签名,保证了客户端的请求签名是由我们后台控制的,我们可以为不同的客户端颁发不同的appSecret。

我们再来看看验证头部参数

图片

上图其实就是验证是否传入值;不过其实有个很重要的一点,就是对此请求进行时间验证,如果大于10分钟表示此链接已经超时,防止别人来到这个链接去请求。这个就是防止盗链。

我们在来看看,如何获取各个参数

图片

图片

上面我们获取了各个参数,相对比较简单;我们在来看看生成sign,和验证sign

图片

上面的流程中,会有个额外的安全处理,

· 防止盗链,我们可以让链接有失效时间

· 利用nonce参数,防止重复提交

在签名验证成功后,判断是否重复提交;

原理就是结合redis,判断是否已经提交过

图片

总结

今天我们用签名的方式,对我们对外提供的接口起到了保护作用;但这种保护仅仅做到了防止别人篡改请求,或者模拟请求。

但是还是缺少对数据自身的安全保护,即请求的参数和返回的数据都是有可能被别人拦截获取的,而这些数据又是明文的,所以只要被拦截,就能获得相应的业务数据。

最后

觉得不错小伙伴记得三连支持哦,后续会持续更新精选技术文章!

这里整理了最近BAT最新面试题,2021船新版本!!需要的朋友可以点击:这个,点这个!!,备注:jj。希望那些有需要朋友能在今年第一波招聘潮找到一个自己满意顺心的工作!
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot如何自定义参数解析器--HandlerM

发表于 2021-01-22

SpringBoot 参数解析 HandlerMethodArgumentResolver

SpringMVC提供了各种姿势的http参数解析支持,GET/POST参数解析篇也可以看到,加一个@RequsetParam注解就可以将方法参数与http参数绑定,看到这时自然就会好奇这是怎么做到的,我们能不能自己定义一种参数解析规则呢?

将介绍如何实现自定义的参数解析,并让其生效

I. 环境搭建

首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;

创建一个maven项目,pom文件如下

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
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

II. 自定义参数解析器

对于如何自定义参数解析器,一个较推荐的方法是,先搞清楚springmvc接收到一个请求之后完整的处理链路,然后再来看在什么地方,什么时机,来插入自定义参数解析器,无论是从理解还是实现都会简单很多。遗憾的是,本篇主要目标放在的是使用角度,所以这里只会简单的提一下参数解析的链路,具体的深入留待后续的源码解析

1. 参数解析链路

http请求流程图:

既然是参数解析,所以肯定是在方法调用之前就会被触发,在Spring中,负责将http参数与目标方法参数进行关联的,主要是借助org.springframework.web.method.support.HandlerMethodArgumentResolver类来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码/**
* Iterate over registered {@link HandlerMethodArgumentResolver}s and invoke the one that supports it.
* @throws IllegalStateException if no suitable {@link HandlerMethodArgumentResolver} is found.
*/
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

上面这段核心代码来自org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument,主要作用就是获取一个合适的HandlerMethodArgumentResolver,实现将http参数(webRequest)映射到目标方法的参数上(parameter)

所以说,实现自定义参数解析器的核心就是实现一个自己的HandlerMethodArgumentResolver

2. HandlerMethodArgumentResolver

实现一个自定义的参数解析器,首先得有个目标,我们在get参数解析,当时遇到了一个问题,当传参为数组时,定义的方法参数需要为数组,而不能是List,否则无法正常解析;现在我们则希望能实现这样一个参数解析,以支持上面的场景

为了实现上面这个小目标,我们可以如下操作

a. 自定义注解ListParam

定义这个注解,主要就是用于表明,带有这个注解的参数,希望可以使用我们自定义的参数解析器来解析;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ListParam {
/**
* Alias for {@link #name}.
*/
@AliasFor("name") String value() default "";

/**
* The name of the request parameter to bind to.
*
* @since 4.2
*/
@AliasFor("value") String name() default "";
}

b. 参数解析器ListHandlerMethodArgumentResolver

接下来就是自定义的参数解析器了,需要实现接口HandlerMethodArgumentResolver

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
ini复制代码public class ListHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(ListParam.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
ListParam param = parameter.getParameterAnnotation(ListParam.class);
if (param == null) {
throw new IllegalArgumentException(
"Unknown parameter type [" + parameter.getParameterType().getName() + "]");
}

String name = "".equalsIgnoreCase(param.name()) ? param.value() : param.name();
if ("".equalsIgnoreCase(name)) {
name = parameter.getParameter().getName();
}
String ans = webRequest.getParameter(name);
if (ans == null) {
return null;
}

String[] cells = StringUtils.split(ans, ",");
return Arrays.asList(cells);
}
}

上面有两个方法:

  • supportsParameter就是用来表明这个参数解析器适不适用
+ 实现也比较简单,就是看参数上有没有前面定义的`ListParam`注解
  • resolveArgument 这个方法就是实现将http参数粗转换为目标方法参数的具体逻辑
+ 上面主要是为了演示自定义参数解析器的过程,实现比较简单,默认只支持`List<String>`

3. 注册

上面虽然实现了自定义的参数解析器,但是我们需要把它注册到HandlerMethodArgumentResolver才能生效,一个简单的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码@SpringBootApplication
public class Application extends WebMvcConfigurationSupport {

@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ListHandlerMethodArgumentResolver());
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

4. 测试

为了验证我们的自定义参数解析器ok,我们开两个对比的rest服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@RestController
@RequestMapping(path = "get")
public class ParamGetRest {
/**
* 自定义参数解析器
*
* @param names
* @param age
* @return
*/
@GetMapping(path = "self")
public String selfParam(@ListParam(name = "names") List<String> names, Integer age) {
return names + " | age=" + age;
}

@GetMapping(path = "self2")
public String selfParam2(List<String> names, Integer age) {
return names + " | age=" + age;
}
}

演示demo如下,添加了ListParam注解的可以正常解析,没有添加注解的会抛异常

II. 其他

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis高可用总结:Redis主从复制、哨兵集群、脑裂

发表于 2021-01-22

Redis高可用总结:Redis主从复制、哨兵集群、脑裂…

越努力,越幸运,

本文已收藏在GitHub中JavaCommunity, 里面有面试分享、源码分析系列文章,欢迎收藏,点赞

github.com/Ccww-lx/Jav…

在实际的项目中,服务高可用非常重要,如,当Redis作为缓存服务使用时, 缓解数据库的压力,提高数据的访问速度,提高网站的性能 ,但如果使用Redis 是单机模式运行 ,只要一个服务器宕机就不可以提供服务,这样会可能造成服务效率低下,甚至出现其相对应的服务应用不可用。

因此为了实现高可用,Redis 提供了哪些高可用方案?

  • Redis主从复制
  • Redis持久化
  • 哨兵集群
  • …

Redis基于一个Master主节点多Slave从节点的模式和Redis持久化机制,将一份数据保持在多个实例中实现增加副本冗余量,又使用哨兵机制实现主备切换, 在master故障时,自动检测,将某个slave切换为master,最终实现Redis高可用 。

Redis主从复制

Redis主从复制,主从库模式一个Master主节点多Slave从节点的模式,将一份数据保存在多Slave个实例中,增加副本冗余量,当某些出现宕机后,Redis服务还可以使用。

但是这会存在数据不一致问题,那redis的副本集是如何数据一致性?

Redis为了保证数据副本的一致,主从库之间采用读写分离的方式:

  • 读操作:主库、从库都可以执行处理;
  • 写操作:先在主库执行,再由主库将写操作同步给从库。

使用读写分离方式的好处,可以避免当主从库都可以处理写操作时,主从库处理写操作加锁等一系列巨额的开销。

采用读写分离方式,写操作只会在主库中进行后同步到从库中,那主从库是如何同步数据的呢?

主从库是同步数据方式有两种:

  • 全量同步:通常是主从服务器刚刚连接的时候,会先进行全量同步
  • 增量同步 :一般在全同步结束后,进行增量同步,比如主从库间网络断,再进行数据同步。

全量同步

主从库间第一次全量同步,具体分成三个阶段:

  • 当一个从库启动时,从库给主库发送 psync 命令进行数据同步(psync 命令包含:主库的 runID 和复制进度 offset 两个参数),
  • 当主库接收到psync 命令后将会保存RDB 文件并发送给从库,发送期间会使用缓存区(replication buffer)记录后续的所有写操作 ,从库收到数据后,会先清空当前数据库,然后加载从主库获取的RDB 文件,
  • 当主库完成 RDB 文件发送后,也会把将保存发送RDB文件期间写操作的replication buffer发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

另外,为了分担主库生成 RDB 文件和传输 RDB 文件压力,提高效率,可以使用 “主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

img

增量同步

增量同步,基于环形缓冲区repl_backlog_buffer缓存区实现。

在环形缓冲区,主库会记录自己写到的位置 master_repl_offset ,从库则会记录自己已经读到的位置slave_repl_offset, 主库并通过master_repl_offset 和 slave_repl_offset的差值的数据同步到从库。

主从库间网络断了, 主从库会采用增量复制的方式继续同步,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区,然后主库并通过master_repl_offset 和 slave_repl_offset的差值数据同步到从库。

因为repl_backlog_buffer 是一个环形缓冲区,当在缓冲区写满后,主库会继续写入,此时,会出现什么情况呢?

覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。因此需要关注 repl_backlog_size参数,调整合适的缓冲空间大小,避免数据覆盖,主从数据不一致。

主从复制,除了会出现数据不一致外,甚至可能出现主库宕机的情况,Redis会有主从自主切换机制,那如何实现的呢?

Redis哨兵机制

当主库挂了,redis写操作和数据同步无法进行,为了避免这样情况,可以在主库挂了后重新在从库中选举出一个新主库,并通知到客户端,redis提供了 哨兵机制,哨兵为运行在特殊模式下的 Redis 进程。

Redis会有主从自主切换机制,那如何实现的呢?

哨兵机制是实现主从库自动切换的关键机制,其主要分为三个阶段:

  • 监控:哨兵进程会周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。
  • 选主(选择主库):主库挂了以后,哨兵基于一定规则评分选选举出一个从库实例新的主库 。
  • 通知 : 哨兵会将新主库的信息发送给其他从库,让它们和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的信息广播通知给客户端,让它们把请求操作发到新主库上。

img

其中,在监控中如何判断主库是否处于下线状态?

哨兵对主库的下线判断分为:

  • 主观下线:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态, 如果单哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”
  • 客观下线:在哨兵集群中,基于少数服从多数,多数实例都判定主库已“主观下线”,则认为主库“客观下线”。

为什么会有这两种”主观下线”和“客观下线”的下线状态呢?

由于单机哨兵很容易产生误判,误判后主从切换会产生一系列的额外开销,为了减少误判,避免这些不必要的开销,采用哨兵集群,引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况,

基于少数服从多数原则, 当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线” (可以自定义设置阙值)。

那么哨兵之间是如何互相通信的呢?

哨兵集群中哨兵实例之间可以相互发现,基于 Redis 提供的发布 / 订阅机制(pub/sub 机制),

哨兵可以在主库中发布/订阅消息,在主库上有一个名为“\__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的,而且只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。

哨兵 1连接相关信息(IP端口)发布到“\__sentinel__:hello”频道上,哨兵 2 和 3 订阅了该频道。

哨兵 2 和 3 就可以从这个频道直接获取哨兵 1连接信息,以这样的方式哨兵集群就形成了,实现各个哨兵互相通信。

哨兵集群中各个实现通信后,就可以判定主库是否已客观下线。

在已判定主库已下线后,又如何选举出新的主库?

新主库选举按照一定条件筛选出的符合条件的从库,并按照一定规则对其进行打分,最高分者为新主库。

通常一定条件包括:

  • 从库的当前在线状态,
  • 判断它之前的网络连接状态,通过down-after-milliseconds * num(断开连接次数),当断开连接次数超过阈值,不适合为新主库。

一定规则包括:

  • 从库优先级 , 通过slave-priority 配置项,给不同的从库设置不同优先级,优先级最高的从库得分高
  • 从库复制进度,和旧主库同步程度最接近的从库得分高,通过repl_backlog_buffer缓冲区记录主库 master_repl_offset 和从库slave_repl_offset 相差最小高分
  • 从库 ID 号 , ID 号小的从库得分高。

全都都基于在只有在一定规则中的某一轮评出最高分从库就选举结束,哨兵发起主从切换。

leader哨兵

选举完新的主库后,不能每个哨兵都发起主从切换,需要选举成leader哨兵,那如何选举leader哨兵执行主从切换?

选举leader哨兵,也是基于少数服从多数原则”投票仲裁”选举出来,

  • 当任何一个从库判定主库“主观下线”后,发送命令 s-master-down-by-addr命令发送想要成为Leader的信号,
  • 其他哨兵根据与主机连接情况作出相对的响应,赞成票Y,反对票N,而且如果有多个哨兵发起请求,每个哨兵的赞成票只能投给其中一个,其他只能为反对票。

想要成为Leader 的哨兵,要满足两个条件:

  • 第一,获得半数以上的赞成票;
  • 第二,获得的票数同时还需要大于等于哨兵配置文件中的quorum值。

选举完leader哨兵并新主库切换完毕之后,那么leader哨兵怎么通知客户端?

还是基于哨兵自身的 pub/sub 功能,实现了客户端和哨兵之间的事件通知,客户端订阅哨兵自身消息频道 ,而且哨兵提供的消息订阅频道有很多,不同频道包含了:

事件 相关频道
主库下线事件 +sdown(实例进入“主观下线”状态)-sdown(实例退出“主观下线”状态)+odown(实例进入“客观下线”状态)-odown(实例退出“客观下线”状态)
新主库切换 + switch-master(主库地址发生变化)

其中,当客户端从哨兵订阅消息主从库切换,当主库切换后,端户端就会接收到新主库的连接信息:

1
xml复制代码switch-master <master name> <oldip> <oldport> <newip> <newport>

在这样的方式哨兵就可以通知客户端切换了新库。

基于上述的机制和原理Redis实现了高可用,但也会带了一些潜在的风险,比如数据缺失。

数据问题

Redis实现高可用,但实现期间可能产出一些风险:

  • 主备切换的过程, 异步复制导致的数据丢失
  • 脑裂导致的数据丢失
  • 主备切换的过程,异步复制导致数据不一致

数据丢失-主从异步复制

因为master 将数据复制给slave是异步实现的,在复制过程中,这可能存在master有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。

总结:主库的数据还没有同步到从库,结果主库发生了故障,未同步的数据就丢失了。

数据丢失-脑裂

何为脑裂?当一个集群中的 master 恰好网络故障,导致与 sentinal 通信不上了,sentinal会认为master下线,且sentinal选举出一个slave 作为新的 master,此时就存在两个 master了。

此时,可能存在client还没来得及切换到新的master,还继续写向旧master的数据,当master再次恢复的时候,会被作为一个slave挂到新的master 上去,自己的数据将会清空,重新从新的master 复制数据,这样就会导致数据缺失。

总结:主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。

数据丢失解决方案

数据丢失可以通过合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag 解决,比如

  • min-slaves-to-write 1
  • min-slaves-max-lag 10

如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。

数据不一致

在主从异步复制过程,当从库因为网络延迟或执行复杂度高命令阻塞导致滞后执行同步命令,这样就会导致数据不一致

解决方案: 可以开发一个外部程序来监控主从库间的复制进度(master_repl_offset 和 slave_repl_offset ),通过监控 master_repl_offset 与slave_repl_offset差值得知复制进度,当复制进度不符合预期设置的Client不再从该从库读取数据。

img

总结

Redis使用主从复制、持久化、哨兵机制等实现高可用,需要理解其实现过程,也要明白其带了风险以及解决方案,才能在实际项目更好优化,提升系统的可靠性、稳定性。

谢谢各位点赞,没点赞的点个赞支持支持

最后,微信搜《Ccww技术博客》观看更多文章,也欢迎关注一波

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IntelliJ IDEA快速实现将SpringBoot项目

发表于 2021-01-22

大家知道SpringBoot项目可以打包成jar包直接可以运行,通过java -jar xxx.jar的命令,那么能不能实现Jenkins那种自动部署的功能,方便我们测试呢?答案是可以,笔者在公司的大佬告诉我可以利用docker实现,大致的流程将程序打包成一个docker的镜像文件,然后启动docker的镜像 docker run xxx 命令来启动,下面一起看一下简单idea配置的步骤。

一台已经安装docker的服务器(笔者是centOS)

执行以下的命令,作用开启Docker的远程访问功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码vim /usr/lib/systemd/system/docker.service
# 修改ExecStart这行,ExecStart= xxxxx不要改变直接空格之后添加
# tcp是开启远程访问,unix是本机访问,不开启本机访问,在服务器上使用时会有以下错误
# Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

#重新加载配置文件
systemctl daemon-reload
# 重启服务
systemctl restart docker.service
# 查看端口是否开启 默认端口2375
netstat -nptl
#直接curl看是否生效 如果没有生效显示被拒绝,生效之后显示json
curl http://127.0.0.1:2375/info

笔者的IDEA版本2020.3.1UE版本,默认已经安装docker的插件。版本过低的,自行Google安装。看到下图的结果证明可以连接到安装docker的服务器。表示配置没有问题,如果结果不一样从头操作一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author smallthanks
*/
@SpringBootApplication
@RestController
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

@GetMapping("/index")
public String index() {
return "hello world";
}
}

按照如上代码,创建一个最简单的SpringBoot的Web项目。并在本地运行确认可以访问,确保没有问题。有问题自行Google。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<packaging>jar</packaging>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<repositories>
<!-- 使用阿里云的镜像-->
<repository>
<id>spring</id>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
·····<!-此处为了读者简洁,省去了必要的依赖-->
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
默认的打的jar包名为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dockerfile复制代码# DockerFile的文件内容为,其字段的名称及作用自行Google,并存放于上图的目录
#指定基础镜像,在其上进行定制
FROM java:8
#维护者信息
MAINTAINER SmallThanks <SmallThanks@163.com>
#这里的 /tmp 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层
VOLUME /tmp
#复制上下文目录下的target/demo-1.0.0.jar 到容器里
COPY /target/demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar
#bash方式执行,使demo-1.0.0.jar可访问
#RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。
RUN bash -c "touch /demo-0.0.1-SNAPSHOT.jar"

#声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
EXPOSE 8080

#指定容器启动程序及参数 <ENTRYPOINT> "<CMD>"
ENTRYPOINT ["java","-jar","demo-0.0.1-SNAPSHOT.jar"]

选中Edit Configurations··· 添加一个docker的运行脚本。
上面配置完成之后,我们就可以运行
访问我虚拟机的地址172.16.81.131:28080/index
1
复制代码我们也可以通过其他的手段实现自动部署,但我我还没学会。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分布式事务:两阶段提交与三阶段提交

发表于 2021-01-22

在分布式系统中著有 CAP 理论,该理论由加州大学伯克利分校的 Eric Brewer 教授提出,阐述了在一个分布式系统中不可能同时满足 一致性(Consistency)、可用性(Availability),以及 分区容错性(Partition tolerance)。

  • C:一致性

在分布式系统中数据往往存在多个副本,一致性描述的是这些副本中的数据在内容和组织上的一致。

  • A:可用性

可用性描述了系统对用户的服务能力,所谓可用是指在用户容忍的时间范围内返回用户期望的结果。

  • P:分区容错性

分布式系统通常由多个节点构成,由于网络是不可靠的,所以存在分布式集群中的节点因为网络通信故障导致被孤立成一个个小集群的可能性,即网络分区,分区容错性要求在出现网络分区时系统仍然能够对外提供一致性的可用服务。

对于一个分布式系统而言,我们要始终假设网络是不可靠的,因此分区容错性是对一个分布式系统最基本的要求,我们的切入点更多的是尝试在可用性和一致性之间寻找一个平衡点,但这也并非要求我们在系统设计时一直建立在网络出现分区的场景之上,然后对一致性和可用性在选择时非此即彼。实际上 Eric Brewer 在 2012 年就曾指出 CAP 理论证明不能同时满足一致性、可用性,以及分区容错性的观点在实际系统设计指导上存在一定的误导性。传统对于 CAP 理论的理解认为在设计分布式系统时必须满足 P,然后在 C 和 A 之间进行取舍,这是片面的,实际中网络出现分区的可能性还是比较小的,尤其是目前网络环境正在变得越来越好,甚至许多系统都拥有专线支持,所以在网络未出现分区时,还是应该兼顾 A 和 C;另外就是对于一致性、可用性,以及分区容错性三者在度量上也应该有一个评定范围,最简单的以可用性来说,当有多少占比请求出现响应超时才可以被认为是不满足可用性,而不是一出现超时就认为是不可用的;最后我们需要考虑的一点就是分布式系统一般都是一个比较大且复杂的系统,我们应该从更小的粒度上对各个子系统进行评估和设计,而不是简单的从整体上认为需要满足 P,而在 A 和 C 之间做取舍,一些子系统可能需要尽可能同时满足三者。

让分布式集群始终对外提供可用的一致性服务一直是富有挑战和趣味的一项任务。暂且抛开可用性,拿一致性来说,对于关系型数据库我们通常利用事务来保证数据的强一致性,当我们的数据量越来越大,大到单库已经无法承担时,我们不得不采取分库分表的策略对数据库实现水平拆分,构建分布式数据库集群,这样可以将一个数据库的压力分摊到多个数据库,极大的提升了数据库的存储和响应能力,但是拆分之后也为我们使用数据库带来了许多的限制,比如主键的全局唯一、联表查询、数据聚合等等,另外一个相当棘手的问题就是数据库的事务由原先的单库事务变成了现在的分布式事务。

分布式事务的实现并不是很难,比如下文要展开的两阶段提交(2PC:Two-Phrase Commit)和三阶段提交(3PC:Three-Phrase Commit)都给我们提供了思路,但是如果要保证数据的强一致性,并要求对外提供可用的服务,就变成了一个几乎不可能的任务(至少目前是),因此很多分布式系统对于数据强一致性都敬而远之。

两阶段提交协议(2PC:Two-Phrase Commit)

两阶段提交协议的目标在于在分布式系统中保证数据的一致性,许多分布式系统采用该协议提供对分布式事务的支持(提供但不一定有人用,呵呵~)。顾名思义,该协议将一个分布式的事务过程拆分成两个阶段:投票阶段和事务提交阶段。为了让整个数据库集群能够正常的运行,该协议指定了一个“协调者”单点,用于协调整个数据库集群的运行,为了简化描述,我们将数据库里面的各个节点称为“参与者”,三阶段提交协议中同样包含“协调者”和“参与者”这两个定义。

第一阶段:投票阶段

该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:

  1. 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
  2. 事务参与者收到请求之后,执行事务,但不提交,并记录事务日志。
  3. 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。
第二阶段:事务提交阶段

在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:

  1. 所有的参与者回复能够正常执行事务
  2. 一个或多个参与者回复事务执行失败
  3. 协调者等待超时。

对于第一种情况,协调者将向所有的参与者发出提交事务的通知,具体步骤如下:

  1. 协调者向各个参与者发送commit通知,请求提交事务。
  2. 参与者收到事务提交通知之后,执行commit操作,然后释放占有的资源。
  3. 参与者向协调者返回事务commit结果信息。

事务提交时序图

对于第二、三种情况,协调者均认为参与者无法正常成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:

  1. 协调者向各个参与者发送事务rollback通知,请求回滚事务。
  2. 参与者收到事务回滚通知之后,执行rollback操作,然后释放占有的资源。
  3. 参与者向协调者返回事务rollback结果信息。

事务回滚时序图

两阶段提交协议解决的是分布式数据库数据强一致性问题,其原理简单,易于实现,但是缺点也是显而易见的,主要缺点如下:

  • 单点问题

协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,那么就会影响整个数据库集群的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。

  • 同步阻塞

两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。

  • 数据不一致性

两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

三阶段提交协议(2PC:Three-Phrase Commit)

针对两阶段提交存在的问题,三阶段提交协议通过引入一个“预询盘”阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。

第一阶段:can_commit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

  1. 协调者向各个参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复
  2. 各个参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息
第二阶段:pre_commit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有三种:

  1. 所有的参与者都返回确定信息
  2. 一个或多个参与者返回否定信息
  3. 协调者等待超时

针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

  1. 协调者向所有的事务参与者发送事务执行通知
  2. 参与者收到通知后,执行事务,但不提交
  3. 参与者将事务执行情况返回给客户端

在上面的步骤中,如果参与者等待超时,则会中断事务。 针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出预备状态,具体步骤如下:

  1. 协调者向所有事务参与者发送abort通知
  2. 参与者收到通知后,中断事务

事务中断时序图

第三阶段:do_commit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:

  1. 所有的参与者都能正常执行事务
  2. 一个或多个参与者执行事务失败
  3. 协调者等待超时

针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

  1. 协调者向所有参与者发送事务commit通知
  2. 所有参与者在收到通知之后执行commit操作,并释放占有的资源
  3. 参与者向协调者反馈事务提交结果

事务提交时序图

针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:

  1. 协调者向所有参与者发送事务rollback通知
  2. 所有参与者在收到通知之后执行rollback操作,并释放占有的资源
  3. 参与者向协调者反馈事务提交结果

事务回滚时序图

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit。相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性。

在分布式数据库中,如果期望达到数据的强一致性,那么服务基本没有可用性可言,这也是为什么许多分布式数据库提供了跨库事务,但也只是个摆设的原因,在实际应用中我们更多追求的是数据的弱一致性或最终一致性,为了强一致性而丢弃可用性是不可取的。

来源:my.oschina.net/wangzhencha…
欢迎关注公众号 【码农开花】一起学习成长
我会一直分享Java干货,也会分享免费的学习资料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Canal:同步mysql增量数据工具,一篇详解核心知识点

发表于 2021-01-22

老刘是一名即将找工作的研二学生,写博客一方面是总结大数据开发的知识点,一方面是希望能够帮助伙伴让自学从此不求人。由于老刘是自学大数据开发,博客中肯定会存在一些不足,还希望大家能够批评指正,让我们一起进步!

背景

大数据领域数据源有业务库的数据,也有移动端埋点数据、服务器端产生的日志数据。我们在对数据进行采集时根据下游对数据的要求不同,我们可以使用不同的采集工具来进行。今天老刘给大家讲的是同步mysql增量数据的工具Canal,本篇文章的大纲如下:

  1. Canal 的概念
  2. mysql 中主备复制实现原理
  3. Canal 如何从 MySQL 中同步数据
  4. Canal 的 HA 机制设计
  5. 各种数据同步解决方法的简单总结

老刘争取用这一篇文章让大家直接上手 Canal 这个工具,不再花别的时间来学习。

mysql 主备复制实现原理

由于 Canal 是用来同步 mysql 中增量数据的,所以老刘先讲 mysql 的主备复制原理,之后再讲 Canal 的核心知识点。


根据这张图,老刘把 mysql 的主备复制原理分解为如下流程:

  1. 主服务器首先必须启动二进制日志 binlog,用来记录任何修改了数据库数据的事件。
  2. 主服务器将数据的改变记录到二进制 binlog 日志。
  3. 从服务器会将主服务器的二进制日志复制到其本地的中继日志(Relaylog)中。这一步细化的说就是首先从服务器会启动一个工作线程 I/O 线程,I/O 线程会跟主库建立一个普通的客户单连接,然后在主服务器上启动一个特殊的二进制转储(binlog dump)线程,这个 binlog dump 线程会读取主服务器上二进制日志中的事件,然后向 I/O 线程发送二进制事件,并保存到从服务器上的中继日志中。
  4. 从服务器启动 SQL 线程,从中继日志中读取二进制日志,并且在从服务器本地会再执行一次数据修改操作,从而实现从服务器数据的更新。

那么 mysql 主备复制实现原理就讲完了,大家看完这个流程,能不能猜到 Canal 的工作原理?

Canal 核心知识点

Canal 的工作原理

Canal 的工作原理就是它模拟 MySQL slave 的交互协议,把自己伪装为 MySQL slave,向 MySQL master 发动 dump 协议。MySQL master 收到 dump 请求后,就会开始推送 binlog 给 Canal。最后 Canal 就会解析 binlog 对象。

Canal 概念

Canal,美[kəˈnæl],是这样读的,意思是水道/管道/渠道,主要用途就是用来同步 MySQL 中的增量数据(可以理解为实时数据),是阿里巴巴旗下的一款纯 Java 开发的开源项目。

Canal 架构


server 代表一个 canal 运行实例,对应于一个 JVM。
instance 对应于一个数据队列,1 个 canal server 对应 1..n 个 instance
instance 下的子模块:

  1. EventParser:数据源接入,模拟 salve 协议和 master 进行交互,协议解析
  2. EventSink:Parser 和 Store 链接器,进行数据过滤,加工,分发的工作
  3. EventStore:数据存储
  4. MetaManager: 增量订阅&消费信息管理器

到现在 Canal 的基本概念就讲完了,那接下来就要讲 Canal 如何同步 mysql 的增量数据。

Canal 同步 MySQL 增量数据

开启 mysql binlog

我们用 Canal 同步 mysql 增量数据的前提是 mysql 的 binlog 是开启的,阿里云的 mysql 数据库是默认开启 binlog 的,但是如果我们是自己安装的 mysql 需要手动开启 binlog 日志功能。

先找到 mysql 的配置文件:

1
2
3
4
5
ini复制代码etc/my.cnf

server-id=1
log-bin=mysql-bin
binlog-format=ROW

这里有一个知识点是关于 binlog 的格式,老刘给大家讲讲。

binlog 的格式有三种:STATEMENT、ROW、MIXED

  1. ROW 模式(一般就用它)

日志会记录每一行数据被修改的形式,不会记录执行 SQL 语句的上下文相关信息,只记录要修改的数据,哪条数据被修改了,修改成了什么样子,只有 value,不会有 SQL 多表关联的情况。

优点:它仅仅只需要记录哪条数据被修改了,修改成什么样子了,所以它的日志内容会非常清楚地记录下每一行数据修改的细节,非常容易理解。

缺点:ROW 模式下,特别是数据添加的情况下,所有执行的语句都会记录到日志中,都将以每行记录的修改来记录,这样会产生大量的日志内容。
2. STATEMENT 模式

每条会修改数据的 SQL 语句都会被记录下来。

缺点:由于它是记录的执行语句,所以,为了让这些语句在 slave 端也能正确执行,那他还必须记录每条语句在执行过程中的一些相关信息,也就是上下文信息,以保证所有语句在 slave 端被执行的时候能够得到和在 master 端执行时候相同的结果。

但目前例如 step()函数在有些版本中就不能被正确复制,在存储过程中使用了 last-insert-id()函数,可能会使 slave 和 master 上得到不一致的 id,就是会出现数据不一致的情况,ROW 模式下就没有。
3. MIXED 模式

以上两种模式都使用。

Canal 实时同步

  1. 首先我们要配置环境,在 conf/example/instance.properties 下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码 ## mysql serverId
 canal.instance.mysql.slaveId = 1234
 #position info,需要修改成自己的数据库信息
 canal.instance.master.address = 127.0.0.1:3306
 canal.instance.master.journal.name =
 canal.instance.master.position =
 canal.instance.master.timestamp =
 #canal.instance.standby.address =
 #canal.instance.standby.journal.name =
 #canal.instance.standby.position =
 #canal.instance.standby.timestamp =
 #username/password,需要修改成自己的数据库信息
 canal.instance.dbUsername = canal
 canal.instance.dbPassword = canal
 canal.instance.defaultDatabaseName =
 canal.instance.connectionCharset = UTF-8
 #table regex
 canal.instance.filter.regex = .\*\\\\..\*

其中,canal.instance.connectionCharset 代表数据库的编码方式对应到 java 中的编码类型,比如 UTF-8,GBK,ISO-8859-1。

  1. 配置完后,就要启动了
1
2
bash复制代码 sh bin/startup.sh
 关闭使用 bin/stop.sh
  1. 观察日志

一般使用 cat 查看 canal/canal.log、example/example.log
4. 启动客户端

在 IDEA 中业务代码,mysql 中如果有增量数据就拉取过来,在 IDEA 控制台打印出来

在 pom.xml 文件中添加:

1
2
3
4
5
xml复制代码 <dependency>
   <groupId>com.alibaba.otter</groupId>
   <artifactId>canal.client</artifactId>
   <version>1.0.12</version>
 </dependency>

添加客户端代码:

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
ini复制代码public class Demo {
 public static void main(String[] args) {
     //创建连接
     CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("hadoop03", 11111),
             "example", "", "");
     connector.connect();
     //订阅
     connector.subscribe();
     connector.rollback();
     int batchSize = 1000;
     int emptyCount = 0;
     int totalEmptyCount = 100;
     while (totalEmptyCount > emptyCount) {
         Message msg = connector.getWithoutAck(batchSize);
         long id = msg.getId();
         List<CanalEntry.Entry> entries = msg.getEntries();
         if(id == -1 || entries.size() == 0){
             emptyCount++;
             System.out.println("emptyCount : " + emptyCount);
             try {
                 Thread.sleep(3000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }else{
             emptyCount = 0;
             printEntry(entries);
         }
         connector.ack(id);
     }
 }
 // batch -> entries -> rowchange - rowdata -> cols
 private static void printEntry(List<CanalEntry.Entry> entries) {
     for (CanalEntry.Entry entry : entries){
         if(entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                 entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND){
             continue;
         }
         CanalEntry.RowChange rowChange = null;
         try {
             rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
         } catch (InvalidProtocolBufferException e) {
             e.printStackTrace();
         }
         CanalEntry.EventType eventType = rowChange.getEventType();
         System.out.println(entry.getHeader().getLogfileName()+" __ " +
                 entry.getHeader().getSchemaName() + " __ " + eventType);
         List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
         for(CanalEntry.RowData rowData : rowDatasList){
             for(CanalEntry.Column column: rowData.getAfterColumnsList()){
                 System.out.println(column.getName() + " - " +
                         column.getValue() + " - " +
                         column.getUpdated());
             }
         }
     }
 }
}
  1. 在mysql中写数据,客户端就会把增量数据打印到控制台。

Canal 的 HA 机制设计

在大数据领域很多框架都会有 HA 机制,Canal 的 HA 分为两部分,Canal server 和 Canal client 分别有对应的 HA 实现:

  1. canal server:为了减少对 mysql dump 的请求,不同 server 上的 instance 要求同一时间只能有一个处于 running,其他的处于 standby 状态。
  2. canal client:为了保证有序性,一份 instance 同一时间只能由一个 canal client 进行 get/ack/rollback 操作,否则客户端接收无法保证有序。

整个 HA 机制的控制主要是依赖了 ZooKeeper 的几个特性,ZooKeeper 这里就不讲了。

Canal Server:

  1. canal server 要启动某个 canal instance 时都先向 ZooKeeper 进行一次尝试启动判断(创建 EPHEMERAL 节点,谁创建成功就允许谁启动)。
  2. 创建 ZooKeeper 节点成功后,对应的 canal server 就启动对应的 canal instance,没有创建成功的 canal instance 就会处于 standby 状态。
  3. 一旦 ZooKeeper 发现 canal server 创建的节点消失后,立即通知其他的 canal server 再次进行步骤 1 的操作,重新选出一个 canal server 启动 instance。
  4. canal client 每次进行 connect 时,会首先向 ZooKeeper 询问当前是谁启动了 canal instance,然后和其建立连接,一旦连接不可用,会重新尝试 connect。
  5. canal client 的方式和 canal server 方式类似,也是利用 ZooKeeper 的抢占 EPHEMERAL 节点的方式进行控制。

Canal HA 的配置,并把数据实时同步到 kafka 中。

  1. 修改 conf/canal.properties 文件
1
2
3
ini复制代码 canal.zkServers = hadoop02:2181,hadoop03:2181,hadoop04:2181
 canal.serverMode = kafka
 canal.mq.servers = hadoop02:9092,hadoop03:9092,hadoop04:9092
  1. 配置 conf/example/example.instance
1
2
ini复制代码  canal.instance.mysql.slaveId = 790 /两台canal server的slaveID唯一
  canal.mq.topic = canal_log //指定将数据发送到kafka的topic

数据同步方案总结

讲完了 Canal 工具,现在给大家简单总结下目前常见的数据采集工具,不会涉及架构知识,只是简单总结,让大家有个印象。

常见的数据采集工具有:DataX、Flume、Canal、Sqoop、LogStash 等。

DataX (处理离线数据)

DataX 是阿里巴巴开源的一个异构数据源离线同步工具,异构数据源离线同步指的是将源端数据同步到目的端,但是端与端的数据源类型种类繁多,在没有 DataX 之前,端与端的链路将组成一个复杂的网状结构,非常零散无法把同步核心逻辑抽象出来。


为了解决异构数据源同步问题,DataX 将复杂的网状的同步链路变成了星型数据链路,DataX 作为中间传输载体负责连接各种数据源。

所以,当需要接入一个新的数据源的时候,只需要将此数据源对接到 DataX,就可以跟已有的数据源做到无缝数据同步。


DataX本身作为离线数据同步框架,采用Framework+plugin架构构建。将数据源读取和写入抽象成为Reader/Writer插件,纳入到整个同步框架中。

  1. Reader: 它为数据采集模块,负责采集数据源的数据,将数据发送给Framework。
  2. Writer: 它为数据写入模块,负责不断向Framework取数据,并将数据写入到目的端。
  3. Framework:它用于连接Reader和Writer,作为两者的数据传输通道,并处理缓冲、并发、数据转换等问题。

DataX的核心架构如下图:


核心模块介绍:

  1. DataX完成单个数据同步的作业,我们把它称之为Job,DataX接收到一个Job之后,将启动一个进程来完成整个作业同步过程。
  2. DataX Job启动后,会根据不同的源端切分策略,将Job切分成多个小的Task(子任务),以便于并发执行。
  3. 切分多个Task之后,DataX Job会调用Scheduler模块,根据配置的并发数据量,将拆分成的Task重新组合,组装成TaskGroup(任务组)。每一个TaskGroup负责以一定的并发运行完毕分配好的所有Task,默认单个任务组的并发数量为5。
  4. 每一个Task都由TaskGroup负责启动,Task启动后,会固定启动Reader->Channel->Writer的线程来完成任务同步工作。
  5. DataX作业运行完成之后,Job监控并等待多个TaskGroup模块任务完成,等待所有TaskGroup任务完成后Job成功退出。否则,异常退出。

Flume(处理实时数据)


Flume主要应用的场景是同步日志数据,主要包含三个组件:Source、Channel、Sink。

Flume最大的优点就是官网提供了丰富的Source、Channel、Sink,根据不同的业务需求,我们可以在官网查找相关配置。另外,Flume还提供了自定义这些组件的接口。

Logstash(处理离线数据)


Logstash就是一根具备实时数据传输能力的管道,负责将数据信息从管道的输入端传输到管道的输出端;与此同时这根管道还可以让你根据自己的需求在中间加上过滤网,Logstash提供了很多功能强大的过滤网来满足各种应用场景。

Logstash是由JRuby编写,使用基于消息的简单架构,在JVM上运行。在管道内的数据流称之为event,它分为inputs阶段、filters阶段、outputs阶段。

Sqoop(处理离线数据)


Sqoop是Hadoop和关系型数据库之间传送数据的一种工具,它是用来从关系型数据库如MySQL到Hadoop的HDFS从Hadoop文件系统导出数据到关系型数据库。Sqoop底层用的还是MapReducer,用的时候一定要注意数据倾斜。

总结

老刘本篇文章主要讲述了Canal工具的核心知识点及其数据采集工具的对比,其中数据采集工具只是大致讲了讲概念和应用,目的也是让大家有个印象。老刘敢做保证看完这篇文章基本等于入门,剩下的就是练习了。

好啦,同步mysql增量数据的工具Canal的内容就讲完了,尽管当前水平可能不及各位大佬,但老刘会努力变得更加优秀,让各位小伙伴自学从此不求人!

如果有相关问题,联系公众号:努力的老刘。文章都看到这了,点赞关注支持一波!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于Android的MVI架构:从双向绑定到单向数据流 何为

发表于 2021-01-22

在这里插入图片描述


现在从事Android开发多少都要懂点架构知识,从MVC、MVP再到MVVM,想必大家对于其各自的优缺点早已如数家珍。今天介绍的MVI与MVVM非常接近,可以针对性地弥补MVVM中的一些缺陷

何为MVI?

在这里插入图片描述
MVI即Model-View-Intent,它受Cycle.js前端框架的启发,提倡一种单向数据流的设计思想,非常适合数据驱动型的UI展示项目:

  • Model: 与其他MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。当前界面展示的内容无非就是UI状态的一个快照:例如数据加载过程、控件位置等都是一种UI状态
  • View: 与其他MVX中的View一致,可能是一个Activity、Fragment或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(不是Activity的Intent、后面介绍)
  • Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model进行数据请求

单向数据流

用户操作以Intent的形式通知Model => Model基于Intent更新State => View接收到State变化刷新UI。

数据永远在一个环形结构中单向流动,不能反向流动:
在这里插入图片描述
这种单向数据流结构的MVI有什么优缺点呢?

  • 优点
+ UI的所有变化来自State,所以只需聚焦State,架构更简单、易于调试
+ 数据单向流动,很容易对状态变化进行跟踪和回溯
+ state实例都是不可变的,确保线程安全
+ UI只是反应State的变化,没有额外逻辑,可以被轻松替换或复用
  • 缺点
+ 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
+ state是不变的,每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销
+ 有些事件类的UI变化不适合用state描述,例如弹出一个toast或者snackbar

talk is cheap, show me the code。

我们通过一个Sample看一下如何快速搭建一个MVI架构的项目。

代码示例

代码结构如下:

Sample中的依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码// Added Dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'

//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"

代码中使用以下API进行请求

1
kotlin复制代码https://reqres.in/api/users

将得到结果:
在这里插入图片描述

  1. 数据层

1.1 User

定义User的data class

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码package com.my.mvi.data.model

data class User(
@Json(name = "id")
val id: Int = 0,
@Json(name = "first_name")
val name: String = "",
@Json(name = "email")
val email: String = "",
@Json(name = "avator")
val avator: String = ""
)

1.2 ApiService

定义ApiService,getUsers方法进行数据请求

1
2
3
4
5
6
7
kotlin复制代码package com.my.mvi.data.api

interface ApiService {

@GET("users")
suspend fun getUsers(): List<User>
}

1.3 Retrofit

创建Retrofit实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码
object RetrofitBuilder {

private const val BASE_URL = "https://reqres.in/api/user/1"

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()


val apiService: ApiService = getRetrofit().create(ApiService::class.java)

}

1.4 Repository

定义Repository,封装API请求的具体实现

1
2
3
4
5
6
7
kotlin复制代码package com.my.mvi.data.repository

class MainRepository(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()

}
  1. UI层

Model定义完毕后,开始定义UI层,包括View、ViewModel以及Intent的定义

2.1 RecyclerView.Adapter

首先,需要一个RecyclerView来呈现列表结果,定义MainAdapter如下:

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
kotlin复制代码package com.my.mvi.ui.main.adapter

class MainAdapter(
private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {

class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
itemView.textViewUserName.text = user.name
itemView.textViewUserEmail.text = user.email
Glide.with(itemView.imageViewAvatar.context)
.load(user.avatar)
.into(itemView.imageViewAvatar)
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DataViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_layout, parent,
false
)
)

override fun getItemCount(): Int = users.size

override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
holder.bind(users[position])

fun addData(list: List<User>) {
users.addAll(list)
}

}

item_layout.xml

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
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="60dp">

<ImageView
android:id="@+id/imageViewAvatar"
android:layout_width="60dp"
android:layout_height="0dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserName"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
app:layout_constraintTop_toTopOf="parent"/>

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textViewUserName"
app:layout_constraintTop_toBottomOf="@+id/textViewUserName" />

</androidx.constraintlayout.widget.ConstraintLayout>

2.2 Intent

定义Intent用来包装用户Action

1
2
3
4
5
6
7
kotlin复制代码package com.my.mvi.ui.main.intent

sealed class MainIntent {

object FetchUser : MainIntent()

}

2.3 State

定义UI层的State结构体

1
2
3
4
5
6
7
8
kotlin复制代码sealed class MainState {

object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()

}

2.4 ViewModel

ViewModel是MVI的核心,存放和管理State,同时接受Intent并进行数据请求

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
kotlin复制代码package com.my.mvi.ui.main.viewmodel

class MainViewModel(
private val repository: MainRepository
) : ViewModel() {

val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state

init {
handleIntent()
}

private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}

private fun fetchUser() {
viewModelScope.launch {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}

我们在handleIntent中订阅userIntent并根据Action类型执行相应操作。本case中当出现FetchUser的Action时,调用fetchUser方法请求用户数据。用户数据返回后,会更新State,MainActivity订阅此State并刷新界面。

2.5 ViewModelFactory

构造ViewModel需要Repository,所以通过ViewModelFactory注入必要的依赖

1
2
3
4
5
6
7
8
9
10
kotlin复制代码class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiService)) as T
}
throw IllegalArgumentException("Unknown class name")
}

}

2.6 定义MainActivity

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
kotlin复制代码package com.my.mvi.ui.main.view

class MainActivity : AppCompatActivity() {

private lateinit var mainViewModel: MainViewModel
private var adapter = MainAdapter(arrayListOf())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
observeViewModel()
setupClicks()
}

private fun setupClicks() {
buttonFetchUser.setOnClickListener {
lifecycleScope.launch {
mainViewModel.userIntent.send(MainIntent.FetchUser)
}
}
}


private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.run {
addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
}
recyclerView.adapter = adapter
}


private fun setupViewModel() {
mainViewModel = ViewModelProviders.of(
this,
ViewModelFactory(
ApiHelperImpl(
RetrofitBuilder.apiService
)
)
).get(MainViewModel::class.java)
}

private fun observeViewModel() {
lifecycleScope.launch {
mainViewModel.state.collect {
when (it) {
is MainState.Idle -> {

}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}

is MainState.Users -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}

private fun renderList(users: List<User>) {
recyclerView.visibility = View.VISIBLE
users.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }
adapter.notifyDataSetChanged()
}
}

MainActivity中订阅mainViewModel.state,根据State处理各种UI显示和刷新。

activity_main.xml:

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
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.view.MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/buttonFetchUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fetch_user"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

如上,一个完整的MVI项目就完成了。

最后

MVI在MVVM的基础上,规定了数据的单向流动和状态的不可变性,这类似于前端的Redux思想,非常适合UI展示类的场景。MVVM也好,MVI也好都不是架构的最终形态,世界上没有完美的架构,要根据项目情况选择适合的架构进行开发。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

极简策略模式20(妙用枚举) 吐槽 求索 码一码(故事纯属

发表于 2021-01-22

吐槽

要先吐槽!先得吃瓜!请移步稍微阅读一哈上一篇关于极简策略模式的文章,深入笔者的心路历程,一起思考,一起学习,一起嗨皮!戳–>极简策略模式1.0…咳咳,我们严肃点,毕竟是程序员!要木讷刻板,笔者写完文章后,也颇为自得,发给了同学、朋友、某神奇的群,然后友好的程序员朋友们,就欢快的吐槽了起来,气氛一下子就起来了,他们主要指出(攻击)了以下的问题:

  1. 钩子与处理逻辑使用Pair结合太过麻烦(笔者觉得他们是在挑刺)
  2. 使用List来存放策略是可以的,但是不觉得泛型写得有点多么(又不是你手写,你怕啥?)
  3. 策略初始化以及使用非常不明显,层次不清晰,冗余复杂(笔者也觉得,我内心只承认这一点)

求索

听完大家友好的反馈以后,我很泄气,决定放弃,全文完…

别急,笔者在某种机缘巧合之下看到了一些关于枚举的文章,发现枚举很妙,有了一些大胆的想法

  1. 枚举在加载时已经初始化,能有效的将使用和初始化分离
  2. 枚举结合的策略方法可以隐藏策略实现的细节,封装相关逻辑
  3. 枚举的增加和修改是非常清晰的,枚举的名称可以有效的指出当前策略的逻辑

码一码(故事纯属虚构,如有雷同,你肯定抄我的)

年关将近,大家关心的当然是怎么给爽子洗白,哦不是,是川普离职了下家去哪里,不!!!!最关心的是打工人
能不能回家过年,笔者也是个打工人,灵机一动,给村里提供了一套先进的系统,过滤打工人的回乡请求,具体的逻辑实现如下面所示

1.村里直接拦路了,打工人直接原路返回

每一次拦路处理的逻辑抽象应该是这样的

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
java复制代码public interface ConsumerStrategy<P, C> {

/**
* 暴露当前策略的钩子
*
* @return 判断钩子
*/
Predicate<P> predicate();

/**
* 暴露当前策略的消费逻辑
*
* @return 消费逻辑
*/
Consumer<C> consumer();

/**
* 真实的消费数据
*
* @param p 钩子来源
* @param c 消费来源
*/
default void handle(P p, C c) {
if (this.predicate().test(p)) {
this.consumer().accept(c);
}
}
}

系统维护了一套拦路询问逻辑,具体逻辑如下

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
java复制代码/**
* 简化处理
* String 代表来自区域风险系数
* Integer 打工人
*/
enum ReturnHomeStrategy implements ConsumerStrategy<String, Integer> {
/**
* 高风险
*/
HIGH_RISK(from -> "HIGH_RISK".equals(from), i -> {
throw new RuntimeException("滚!");
}),
/**
* 中风险
*/
MIDDLE_RISK(from -> "MIDDLE_RISK".equals(from), i -> {
throw new RuntimeException("滚!");
}),
/**
* 低风险
*/
LOW_RISK(from -> "LOW_RISK".equals(from), i -> {
//todo 如果可以,也请滚
//todo 核算检测
//todo 居家隔离
});

private final Predicate<String> predicate;

private final Consumer<Integer> consumer;

ReturnHomeStrategy(Predicate<String> predicate, Consumer<Integer> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}

@Override
public Predicate<String> predicate() {
return this.predicate;
}

@Override
public Consumer<Integer> consumer() {
return this.consumer;
}
}

系统运行情况如下

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 某人回家
*
* @param from 代表来自区域风险系数
* @param id 打工人
*/
public void returnHome(String from, Integer id) {
for (ReturnHomeStrategy value : ReturnHomeStrategy.values()) {
value.handle(from, id);
}
}

系统运行了一段时间以后,广受好评,成功的保护了笔者村庄的安全,只是村长觉得,不够人性化,没有人文主义关怀,问笔者有没有好的方法,笔者肚子一晃,坏水一抖,说要不给他们开一个证明吧,有理有据的那种,体现村里的关爱,村长深深的看了笔者一眼,微微点了下头

2.村里直接拦路了,但是给开了书面证明,系统2.0上线了

改版以后,每一次拦路处理的逻辑抽象应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface FunctionStrategy<P, T, R> {

/**
* 暴露当前策略的钩子
*
* @return 判断钩子
*/
Predicate<P> predicate();

/**
* 暴露当前策略的生产逻辑
*
* @return 消费逻辑
*/
Function<T, R> function();
}

系统维护新的拦路并签发书面证明的逻辑

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
java复制代码/**
* 简化处理
* String 代表来自区域风险系数
* Integer 打工人
* String 村里开具证明
*/
enum ReturnHomeStrategy implements FunctionStrategy<String, Integer, String> {
/**
* 高风险
*/
HIGH_RISK(from -> "HIGH_RISK".equals(from), i -> "滚!"),
/**
* 中风险
*/
MIDDLE_RISK(from -> "MIDDLE_RISK".equals(from), i -> "滚!"),
/**
* 低风险
*/
LOW_RISK(from -> "LOW_RISK".equals(from), i -> "村里的祸害回来了,给他做个核算检测!");

private final Predicate<String> predicate;

private final Function<Integer, String> function;

public Predicate<String> getPredicate() {
return predicate;
}

public Function<Integer, String> getFunction() {
return function;
}

ReturnHomeStrategy(Predicate<String> predicate, Function<Integer, String> function) {
this.predicate = predicate;
this.function = function;
}

@Override
public Predicate<String> predicate() {
return this.predicate;
}

@Override
public Function<Integer, String> function() {
return this.function;
}
}

系统再次运行情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* 某人回家
*
* @param from 代表来自区域风险系数
* @param id 打工人
* @return 要证明
*/
public String returnHome(String from, Integer id) {
for (ReturnHomeStrategy value : ReturnHomeStrategy.values()) {
if (value.predicate().test(from)) {
return value.function().apply(id);
}
}
throw new RuntimeException("外星人,抓起来放进动物园卖门票!");
}

系统终于完美的上线了,世界真美好,空气真清新,就是村里笔者爱的小花也被拦在了村外,真是让人惆怅

正经总结

愚者千虑,必有一得笔者一直在思考各种设计模式的简化版的写法,比如原来也写过管道模式的简化版(别去看,思考欠佳,怕丢人),本篇文章也是多次思考修改所得,也是希望大家能够获得一些灵感,让设计模式不再是重构甚至重开项目时的选择,让策略简单化,成为我们能用,易用,随心用的代码模板,增加编码效率,拥抱健康生活,祝好,谢谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…731732733…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%