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

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


  • 首页

  • 归档

  • 搜索

告别jodatime!拥抱Java8日期时间类的最佳实践 1

发表于 2021-03-29

1 为什么需要新的日期和时间库?

Java开发人员的一个长期烦恼是对普通开发人员的日期和时间用例的支持不足。

例如,现有的类(例如java.util.Date和SimpleDateFormatter)是非线程安全的,从而导致用户潜在的并发问题,这不是一般开发人员在编写日期处理代码时会期望处理的问题。
一些日期和时间类还表现出相当差的API设计。例如,年份java.util.Date从1900开始,月份从1开始,天从0开始,这不是很直观。

这些问题以及其他一些问题导致第三方日期和时间库(例如Joda-Time)的欣欣向荣。

为了解决这些问题并在JDK内核中提供更好的支持,针对Java SE 8设计了一个新的没有这些问题的日期和时间API。该项目由Joda-Time(Stephen Colebourne)和Oracle的作者在JSR 310下共同领导,出现在Java SE 8软件包中java.time。

2 核心思想

不可变值类

Java现有格式化程序的严重缺陷之一是它们不是线程安全的。这给开发人员带来了负担,使其需要以线程安全的方式使用它们并在其日常处理日期处理代码的过程中考虑并发问题。新的API通过确保其所有核心类都是不可变的并表示定义明确的值来避免此问题。

域驱动

新的API模型与代表不同的用例类域非常精确Date和Time严密。这与以前的Java库不同,后者在这方面很差。例如,java.util.Date在时间轴上表示一个时刻(一个自UNIX纪元以来的毫秒数的包装器),但如果调用toString(),结果表明它具有时区,从而引起开发人员之间的困惑。

这种对域驱动设计的重视在清晰度和易理解性方面提供了长期利益,但是当从以前的API移植到Java SE 8时,您可能需要考虑应用程序的域日期模型。

按时间顺序分隔

新的API使人们可以使用不同的日历系统来满足世界某些地区(例如日本或泰国)用户的需求,而这些用户不一定遵循ISO-8601。这样做不会给大多数开发人员带来额外负担,他们只需要使用标准的时间顺序即可。

3 LocalDate、LocalTime、LocalDateTime

3.1 相比 Date 的优势

  • Date 和 SimpleDateFormatter 非线程安全,而 LocalDate 和 LocalTime 和 String 一样,是final类型 - 线程安全且不能被修改。
  • Date 月份从0开始,一月是0,十二月是11。LocalDate 月份和星期都改成了 enum ,不会再用错。
  • Date是一个“万能接口”,它包含日期、时间,还有毫秒数。如果你只需要日期或时间那么有一些数据就没啥用。在新的Java 8中,日期和时间被明确划分为 LocalDate 和 LocalTime,LocalDate无法包含时间,LocalTime无法包含日期。当然,LocalDateTime才能同时包含日期和时间。
  • Date 推算时间(比如往前推几天/ 往后推几天/ 计算某年是否闰年/ 推算某年某月的第一天、最后一天、第一个星期一等等)要结合Calendar要写好多代码,十分恶心!

两个都是本地的,因为它们从观察者的角度表示日期和时间,例如桌子上的日历或墙上的时钟。

还有一种称为复合类LocalDateTime,这是一个LocalDate和LocalTime的配对。

时区将不同观察者的上下文区分开来,在这里放在一边;不需要上下文时,应使用这些本地类。这些类甚至可以用于表示具有一致时区的分布式系统上的时间。

常用 API

now()

获取在默认的时区系统时钟内的当前日期。该方法将查询默认时区内的系统时钟,以获取当前日期。
使用该方法将防止使用测试用的备用时钟,因为时钟是硬编码的。

方便的加减年月日,而不必亲自计算!

plusMonths

返回此副本LocalDate添加了几个月的指定数目。
此方法将分三步指定金额的几个月字段:

  • 将输入的月数加到month-of-year字段
  • 校验结果日期是否无效
  • 调整 day-of-month ,如果有必要的最后有效日期

例如,2007-03-31加一个月会导致无效日期2007年4月31日。并非返回一个无效结果,而是 2007-04-30才是最后有效日期。调用实例的不可变性不会被该方法影响。

4 创建对象

工厂方法

新API中的所有核心类都是通过熟练的工厂方法构造。

  • 当通过其构成域构造值时,称为工厂of
  • 从其他类型转换时,工厂称为from
  • 也有将字符串作为参数的解析方法。

getter约定

  • 为了从Java SE 8类获取值,使用了标准的Java getter约定,如下:

更改对象值

也可以更改对象值以执行计算。因为新API中所有核心类都是不可变的,所以将调用这些方法with并返回新对象,而不是使用setter。也有基于不同字段的计算方法。

调整器

新的API还具有调整器的概念—一块代码块,可用于包装通用处理逻辑。可以编写一个WithAdjuster,用于设置一个或多个字段,也可编写一个PlusAdjuster用于添加或减去某些字段。值类还可以充当调节器,在这种情况下,它们将更新它们表示的字段的值。内置调节器由新的API定义,但是如果您有想要重用的特定业务逻辑,则可以编写自己的调节器。

1
2
3
4
5
6
7
8
java复制代码import static java.time.temporal.TemporalAdjusters.*;

LocalDateTime timePoint = ...
foo = timePoint.with(lastDayOfMonth());
bar = timePoint.with(previousOrSame(ChronoUnit.WEDNESDAY));

// 使用值类作为调整器
timePoint.with(LocalTime.now());

5 截断

新的API通过提供表示日期,时间和带时间的日期的类型来支持不同的精确度时间点,但是显然,精确度的概念比此精确度更高。

该truncatedTo方法存在支持这种使用情况下,它可以让你的值截断到字段,如下

1
java复制代码LocalTime truncatedTime = time.truncatedTo(ChronoUnit.SECONDS);

6 时区

我们之前查看的本地类抽象了时区引入的复杂性。时区是一组规则,对应于标准时间相同的区域。大约有40个。时区由它们相对于协调世界时(UTC,Coordinated Universal Time)的偏移量定义。它们大致同步移动,但有一定差异。

时区可用两个标识符来表示:缩写,例如“ PLT”,更长的例如“ Asia / Karachi”。在设计应用程序时,应考虑哪种情况适合使用时区,什么时候需要偏移量。

  • ZoneId是区域的标识符。每个ZoneId规则都对应一些规则,这些规则定义了该位置的时区。在设计软件时,如果考虑使用诸如“ PLT”或“ Asia / Karachi”之类的字符串,则应改用该域类。一个示例用例是存储用户对其时区的偏好。
  • ZoneOffset是格林威治/ UTC与时区之间的差异的时间段。可在特定的ZoneId,在特定时间被解析,如清单7所示。
1
java复制代码ZoneOffset offset = ZoneOffset.of("+2:00");

7 时区类

ZonedDateTime是具有完全限定时区的日期和时间。这样可以解决任何时间点的偏移。
最佳实践:若要表示日期和时间而不依赖特定服务器的上下文,则应使用ZonedDateTime。

1
java复制代码ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");

OffsetDateTime是具有已解决偏移量的日期和时间。这对于将数据序列化到数据库中很有用,如果服务器在不同时区,则还应该用作记录时间戳的序列化格式。

OffsetTime 是具有确定的偏移量的时间,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码OffsetTime time = OffsetTime.now();
// changes offset, while keeping the same point on the timeline
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(
offset);
// changes the offset, and updates the point on the timeline
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(
offset);
// Can also create new object with altered fields as before
changeTimeWithNewOffset
.withHour(3)
.plusSeconds(2);
OffsetTime time = OffsetTime.now();
// changes offset, while keeping the same point on the timeline
OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(
offset);
// changes the offset, and updates the point on the timeline
OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(
offset);
// Can also create new object with altered fields as before
changeTimeWithNewOffset
.withHour(3)
.plusSeconds(2);

Java中已有一个时区类,java.util.TimeZone但Java SE 8并没有使用它,因为所有JSR 310类都是不可变的并且时区是可变的。

8 时间段(period)

Period代表诸如“ 3个月零一天”的值,它是时间线上的距离。这与到目前为止我们讨论过的其他类形成了鲜明的对比,它们是时间轴上的重点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 3 年, 2 月, 1 天
Period period = Period.of(3, 2, 1);

// 使用 period 修改日期值
LocalDate newDate = oldDate.plus(period);
ZonedDateTime newDateTime = oldDateTime.minus(period);
// Components of a Period are represented by ChronoUnit values
assertEquals(1, period.get(ChronoUnit.DAYS));
// 3 years, 2 months, 1 day
Period period = Period.of(3, 2, 1);

// You can modify the values of dates using periods
LocalDate newDate = oldDate.plus(period);
ZonedDateTime newDateTime = oldDateTime.minus(period);
// Components of a Period are represented by ChronoUnit values
assertEquals(1, period.get(ChronoUnit.DAYS));

9 持续时间(Duration)

Duration是时间线上按时间度量的距离,它实现了与相似的目的Period,但精度不同:

1
2
3
4
5
6
java复制代码// 3 s 和 5 ns 的 Duration 
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);
// A duration of 3 seconds and 5 nanoseconds
Duration duration = Duration.ofSeconds(3, 5);
Duration oneDay = Duration.between(today, yesterday);

可以对Duration实例执行常规的加,减和“ with”运算,还可以使用修改日期或时间的值Duration。

10 年表

为了满足使用非ISO日历系统的开发人员的需求,Java SE 8引入了Chronology,代表日历系统,并充当日历系统中时间点的工厂。也有一些接口对应于核心时间点类,但通过

1
2
3
4
5
6
7
8
java复制代码Chronology:
ChronoLocalDate
ChronoLocalDateTime
ChronoZonedDateTime
Chronology:
ChronoLocalDate
ChronoLocalDateTime
ChronoZonedDateTime

这些类仅适用于正在开发高度国际化的应用程序且需要考虑本地日历系统的开发人员,没有这些要求的开发人员不应使用它们。有些日历系统甚至没有一个月或一周的概念,因此需要通过非常通用的字段API进行计算。

11 其余的API

Java SE 8还具有一些其他常见用例的类。有一个MonthDay类,其中包含一对Month和Day,对于表示生日非常有用。该YearMonth类涵盖了信用卡开始日期和到期日期的用例以及人们没有指定日期的场景。

Java SE 8中的JDBC将支持这些新类型,但不会更改公共JDBC API。现有的泛型setObject和getObject方法就足够了。

这些类型可以映射到特定于供应商的数据库类型或ANSI SQL类型。

12 总结

Java SE 8在java.time中附带一个新的日期和时间API,为开发人员提供了大大改善的安全性和功能。新的API很好地建模了该领域,并提供了用于对各种开发人员用例进行建模的大量类。

本文转载自: 掘金

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

LeetCode 专栏

发表于 2021-03-29

开始有计划刷题,刷一些常见的面试题,答案都是我自己能找到并理解的最优解,监督自己,持续更新。

简单

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

image.png
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/be…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0 || prices == null)
return 0;
int minProfit = prices[0];
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (minProfit > prices[i]) {
minProfit = prices[i];
}
if (prices[i] - minProfit > maxProfit) {
maxProfit = prices[i] - minProfit;
}
}
return maxProfit;
}
}

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

image.png
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/me…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode cur = dummyHead;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
cur = cur.next;
l1 = l1.next;
} else {
cur.next = l2;
cur = cur.next;
l2 = l2.next;
}
}
if (l1 == null) {
cur.next = l2;
} else {
cur.next = l1;
}
return dummyHead.next;
}
}

image.png

234. 回文链表

请判断一个链表是否为回文链表。

输入: 1->2

输出: false

输入: 1->2->2->1

输出: true

思路:如果一个链表是回文链表,那么将链表对折,对折后的两个链表上各值应该是相等的。(从今天开始,增加解题思路 20210407)

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
java复制代码class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null)
return true;
ListNode slow = head, fast = head;
// 找中位,对折
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
// 反转后半段
slow = revserseList(slow.next);
// 将对折的两半链表各值做对比
while (slow != null) {
if (head.val != slow.val) {
return false;
}
head = head.next;
slow = slow.next;
}
return true;
}
// 反转链表(专门有一道题是反转链表,这就是答案)
private ListNode revserseList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode pre = null;
while (head != null) {
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}

image.png
哦 对了,今天看到一个校招狠人在面试字节的时候,考算法时采用的是 acm 形式,即编程的一切都要自己手写,比如题中的 ListNode 也要自己定义,今天开始也要记录一下题中的一些定义类型,提前适应节奏。

1
2
3
4
5
6
7
java复制代码  public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

409. 最长回文串

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。

在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。

注意:
假设字符串的长度不会超过 1010。

示例1:

输入:

“abccccdd”

输出:

7

解释:

我们可以构造的最长的回文串是”dccaccd”, 它的长度是 7。
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/lo…

思路:能组成回文字符的一组字符中,里面的字符都是偶数个,如 “abba”。但是我们可以在中间加一个字符,如“abcba”,所以我们先统计出偶数的字符再加 1 就是最长回文字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
public int longestPalindrome(String s) {
int cArr = new int[128];
for (char c : s.toCharArray()) {
cArr[c]++;
}
int count = 0;
for (int i : arr) {
count += (i % 2);
}
return count == 0 ? s.length() : (s.length() - count + 1);

}

}

image.png

1. 两数之和(20210413)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9

输出:[0,1]

解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码class Solution {
public int[] twoSum(int[] nums, int target) {
if (nums == null || nums.length <= 1)
return nums;
int[] res = new int[2];
HashMap<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
int val = target - num;
if (map.containsKey(val)) {
res[0] = i;
res[1] = map.get(val);
return res;
} else {
map.put(num, i);
}
}
return res;
}
}

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/tw…
image.png

中等

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

来源:力扣(LeetCode)链接:leetcode-cn.com/problems/3s…

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,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
java复制代码class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
Set<List<Integer>> res = new HashSet<>();
for (int i = 0; i < n; i++) {
int l = i + 1, r = n - 1;
while (l < r) {
if (nums[i] + nums[l] + nums[r] == 0) {
res.add(Arrays.asList(nums[i], nums[l],nums[r]));
l++;
r--;
} else if (nums[i] + nums[l] + nums[r] < 0) {
l++;
} else {
r--;
}
}
}
List<List<Integer>> ans = new ArrayList<>();
ans.addAll(res);
return ans;
}
}

image.png

103.二叉树的锯齿形层序遍历

给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:
给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
java复制代码    3
/ \
9 20
/ \
15 7

返回锯齿形层序遍历如下:

1
2
3
4
5
java复制代码[
[3],
[20,9],
[15,7]
]
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
java复制代码class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
boolean leftToRight = true;
while (!queue.isEmpty()) {
List<Integer> list = new ArrayList<>();

int n = queue.size();
for (int i = 0; i < n; i++) {
TreeNode node = queue.poll();
if (leftToRight == true) {
list.add(node.val);
} else {
list.add(0, node.val);
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
leftToRight = !leftToRight;
res.add(list);
}
return res;
}
}

image.png

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/bi…

146. LRU 缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/lr…

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
java复制代码class LRUCache {
private int capacity;
private HashMap<Integer,Node> map;
private Node head,tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>(capacity);

head = new Node (0,0);
tail = new Node (0,0);

head.next = tail;
tail.prev = head;
}

public int get(int key) {
// 如果找不到就返回 -1
if (!map.containsKey(key)) {
return -1;
}
// 找到了就返回 value 并将其放到第一位
Node node = map.get(key);
this.moveNode2First(node);
return node.value;
}

public void put(int key, int value) {
// 重复替换
if (map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
this.moveNode2First(node);
return;
}
// 容量满了,就抛弃最后一个
if (map.size() == capacity) {
Node last = tail.prev;
this.map.remove(last.key);
this.removeLast(last);
}
Node node = new Node(key, value);
map.put(key,node);
this.addNode2First(node);
}
public void moveNode2First (Node node) {
Node next = node.next;
Node prev = node.prev;

prev.next = next;
next.prev = prev;
addNode2First(node);
}
public void addNode2First (Node node) {
Node curNode = head.next;
curNode.prev = node;
head.next = node;

node.next = curNode;
node.prev = head;
}
public void removeLast (Node last) {
Node prev = last.prev;
prev.next =tail;
tail.prev = prev;

last.next = null;
last.prev = null;
}
public class Node {
private Node next;
private Node prev;
private int key, value;
public Node(int key, int value){
this.key = key;
this.value = value;
}
}
}

image.png

199.二叉树的右视图(20210409)

给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例:

image.png

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
java复制代码public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(){}
public TreeNode(int val){this.val = val;}
public TreeNode(int val, TreeNode left,TreeNode right){
this.val = val;
this.left = left;
this.right = right;
}
}

class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null)
return res;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
// 添加左节点
if (node.left != null) {
q.offer(node.left);
}
// 添加右节点
if (node.right != null) {
q.offer(node.right);
}
// 将最后一个节点添加到结果集
if (i == size - 1) {
res.add (node.val);
}
}
}
return res;

}
}

image.png
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/bi…

209. 长度最小的子数组 (20210410)

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例1:

输入:target = 7, nums = [2,3,1,2,4,3]

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。

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复制代码class Solution {
// 滑动窗口,先设置左右边界 left、right
public int minSubArrayLen(int target, int[] nums) {
int left = -1, right = -1;
int sum = 0, minLen = Integer.MAX_VALUE;
// 如果和小于 target,那么我们就移动右边界
while (left <= right) {
if (sum < target) {
right++;
if (right >= nums.length) {
break;
}
sum += nums[right];
} else {
// 如果大于等于 targert 我们就移动左边界
// 同时,如果此时数组长度小于最小程度,那么我们更新最小长度。
if (right - left < minLen)
minLen = right - left;
left++;
if (left > right)
break;
sum -= nums[left];
}

}
return minLen == Integer.MAX_VALUE? 0: minLen;
}
}

image.png
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/mi…

困难

25. K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

image.png
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/re…

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
java复制代码class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null)
return head;
ListNode tail = head;
// 想象成将一个长链表分段
for (int i = 0; i < k; i++) {
if (tail == null) return head;
tail = tail.next;
}
ListNode newHead = reverse(head, tail);
head.next = reverseKGroup(tail, k);
return newHead;
}
// 反转局部链表
private ListNode reverse (ListNode head, ListNode tail) {
ListNode pre = null, next = null;
while (head != tail) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}

本文转载自: 掘金

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

互联网黄了,还有toB

发表于 2021-03-29

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

最近有创业的朋友咨询,说手里有钱,但不知道要干些啥。想到的点子,放眼四顾,早已经被人实现了。我推荐他去当网红,但他又比较内向,抹不开面子,还是想在轻车熟路的互联网行业捞一把块钱。

但现在,很难了。

经验并不总是有效的,今日不比往常。一个不小心,就得把过去赚的钱,全部给吐出来。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

谈到互联网,从业者首先会想到2B (toB) 和2C两个概念。B和C两个字母隔得很近,但却有着天囊之别。十年河东十年河西,在2C逐渐没落的今天,很多多金的大佬,已经把目光瞄准了企业服务领域。比如,王兴在2018年就把目光瞄准了B端市场。

只有制造混乱,才能获取利益,在互联网中也是如此。2B行业的乱象,吸引着无数人的目光。

2B行业一直不温不火,主要是由于它的业绩太稳定了,利润率有限,不像2C互联网一样大起大落。巨大的波动吸引着投机者,以及敢于冒险的投资人。他们形成合力,将故事情节一波波推向高潮。

这种情况,在2018年的一哆嗦中,嘎然而止。无数亏损的互联网企业,不求继续画饼了,纷纷忙着上市,希望在资本市场最后收割一场。很难解释这许多企业的统一行为。春江水暖鸭先知,2C什么时候达到高潮,资本最明白。

很多不赚钱的2C公司,错过了2018,后面会活的很痛苦。随后的两年验证了前面的结论,然后进入了寒冬无衣瞎蹦哒的现状。

1、BC的鸿沟

要想深入了解2B和2C的一些特点,必须先了解一下他们的不同之处。两个行业虽同为互联网,但面对的却是两个不同的群体。

下面从四个方面可以下这些差别。

1.1、服务目标相囧

2C和2B有非常多的区别。首先,是服务目标的不同。一个是用户,一个是客户,对待起来自然有所差别。

2C的服务对象,是个人用户,小单大流量,以满足个体的七情六欲为主,所以多会从人性上去考虑产品。做事情先不求回报,以曝光率为主。

很多产品都是凭空想象的,或大众,或小众,只要有目标人群,就有相应的流量。在这种情况下,产品就是上帝。多数产品在调整之后,虽然会丢失一部分老用户,但会获得更多的用户。

这有点像宗教的集体朝圣,或是追星行为,有些幼稚且无理性。

人类生来就是无聊的,在迷醉的狂欢中追求生的意义。 这是2C存在的基础。

到了2B这里,上帝摇身一变,成为了老板,或者客户。2B是为了解决具体的领域问题,或者复杂的流程而存在的,不会那么天马行空,很多东西决策的流程冗长,有具体的存在意义,才能发展下去。

这是2B存在的基础。

这个领域很难暴富,但一旦成型,利润是稳定的,日积月累下来,并不见得赚的少。2B很不适合心浮气躁的人,因为在这种领域下混个三五年,是没什么用的。

1.2、产品特点各异

产品特点的不同。一个是体验式,一个是租用式。

2C的产品,一般都是集中式的。所有的产品逻辑上统一部署,不同的用户,看到的内容基本上是一致的,差异体现在增值服务上。

2C的产品迭代快,上线后的功能,如果发现用户爸爸不喜欢,可能下周就被撤掉了,开发人员会发现自己做了很多无用功。大家伙都在为产品的创意买单。

2B这里,平台性质的是少数。更多公司的服务,以项目的方式进行售卖,产品形态上变化多端。加上客户爸爸的自定义需求,同样会让开发人员疲于奔命。

2B产品的需求,比2C的更加不稳定。这个结论,可能对于很多同学来说,难以理解。

但稳定的功能模块,会有非常长的生命周期,大多数功能迭代会趋向于个性化。客户相对于用户,由于是直接金主,话语权更强。在这种情况下,大家伙都在为项目经理的沟通管理买单。

1.3、商业模式不同

对2C来说,是最主要的营收通道就是广告。当然也有直播抽成、手续费、订阅这样的变种。目的就是汇总小钱,变成大钱。游戏行业会是付费用户养着免费用户,赚钱还是考头部付费用户。但由于服务的对象,还是个体用户,所以形式上虽然不一,但本质上是一样的。

你可以想象一下,你每天都在免费用着微信,免费玩着王者荣耀,但腾讯每年营收的3000多亿是哪里来的?

所以2C互联网非常注重网络运营,有了流量就有了可能性,总会吸引一定概率的人群进行付费。

但2B就可怜的多。传播口径,只在特定范围内存在,售卖主要靠销售、或者靠关系网。主要靠大客户的钱生存。

2B有大B和小B之分,也有混合型的。和2C一样,规模小的服务对象,获得的利益也少;规模大的服务对象,产品话语权会旁落甲方,造成整个团队赚不了多少钱,而且苦闷。

对待不同规模的企业对象,服务配套肯定是不同的。销售拿到的通常都是大单,但钱通常都花的飞快。利润一开始就控制好的,后面会按照步骤一步步走,节外生枝会造成利润不可预料的偏移。因为2B企业除了服务差价所产生的利润,一无所有。

花了100块钱,仅收回50块。或者拿到90块,却花了100块,有病不是?

1.4、研发侧重点不一样

对于2C来说,最大的考验,就是量。尤其是一群营销天才高出秒杀这个玩意以后,类似的概念,无时无刻不考验着系统承受高并发的能力。

流量的不确定性,使得研发基础设施的建设,充满了诸多挑战。各种概念,如微服务、中台等,将研发设施逐一进行抽离,并层级的、网状的划分。

对2B来说,主要的挑战,就是业务的复杂度,以及可复用组件的建设。前者能够确保项目能正常完工,后者能控制最重要的成本因素。

2、「熵(shāng)」

本文算是对2B领域的一点反思。作为一个常年混迹于2C的互联网人,为什么会有这样的想法?

因为从目前的一些数据观测上来看,投入充满乱象的2B产业,会比2C产生更多的收益。宏观层面,全国信息化建设已达到瓶颈,人口红利不再;政策层面,开始倾向于互联网价值的下沉,以及对实体经济反哺;投资层面,烧钱的模式越来越不值得青睐,陈本和收益成为首要目标。

这个过程,我打算使用物理学中的熵进行初步解释。

2.1、混乱变成有序

魔术之所以称之为魔术,因为它违反了自然界的基本定律。在视觉的转换之下,这种明显与经验相悖的东西,总能点燃我们的兴趣,虽然我们知道背后都是些假的把戏。

熵,是热力学中表征物质状态的参量之一。它的物理意义,通俗来说,表现的是热力体系的混乱程度,在控制论、概率论、数论、天体物理、生命科学等领域有重要应用,是十分重要的参量。这也是所有物理体系中,与时间的单向不可逆性相通的唯一的物理概念。

大爆炸理论中,将熵增后的宇宙,描述成毫无生气的一片死寂,任何残存的波动,都会被这种死寂彻底吞没,归于永恒。

冰块融化、酒精挥发、蜡烛燃烧、鸡飞蛋打、破镜不能重圆,也是描述的这种单向关系。事物改变之后,就不能再恢复原样,除非有非常强大的外力作用在上面。

脱离了放任即乱,一管就死的宏观调控,熵化是新事物发展的必然趋势。在混乱之中才有机会,在平静中只有死亡。互联网经历了广告业数据化、内容产业数字化、生活服务数字化、行业数字化等阶段,已经慢慢的由混乱变成了死寂,也是事物发展的必然。今天的互联网,急需要新的技术革命的引爆,否则将毫无悬念的变为传统行业。

2.2、热力现状

先看一个简单的互联网现状。

在2C领域,目前,中国已经度过了C2C(Copy to China)、百花齐放的模式—即中国复制国外的商业模式,进入了存量竞争的阶段。早些年,百度仿造了谷歌、优酷仿造了YouTube,但外卖和新零售却由进口变成了出口。但C端市场的获客难度,加上大厂的技术垄断,几乎断了大部分创业者的梦想。

没有大钱,玩不转了。

2B领域,大厂凭借资金和成本优势,逐渐分食传统的关系链模式。但2B的服务并不好做,有些行业壁垒非常深,并无法做成通用的产品,想要通吃非常困难。这个不管叉腿进来的大厂,还是一些传统企业,逐渐轮化为人力外包。

技术领域,随着云端环境的流行,IaaS、PaaS等基础建设已经不再是企业的竞争优势,更多的公司参与到SaaS行业的竞争中来。但SaaS行业多牛鬼蛇神,客户培养成本居高不下,多数SaaS公司走向没落。

在这互联网逐渐秩序化的现状下,能拿得出手的技术仅有:区块链、人工智能、5G、量子计算机等。它们有的有具体形态,有的依然在襁褓中。先不要去评价这些技术的价值,它们相当于在平静的湖水中扔进的一块石头,对于大部分错过互联网红利期的朋友来说,是唯一的机会。

混乱中的机会。

3、2B的外在形态

我们在上面,谈到了两种互联网形态,在服务目标、产品特点、商业模式上的不同,但其实在两者之间,也有一条模糊的通道。当服务的目标足够小的时候,就是2C;当服务的目标够大的时候,就是2B。

如果你想通吃,不论B、C全部揽到怀里来,往往会消化不良。也就是说,我们一开始,就要找准服务的目标。

如果服务的主要是小型的企业,初创型的企业,那么产品就非常的像SaaS,采用租用的模式获取利润。本质上,还是以量取胜。

如果服务的对象是大企业,非常大的客户,那么产品形态,就是项目。验收完毕,一锤子买卖。

如果服务的对象,领域不一,大小客户都有。那无论公司名叫的再好听,本质上都是外包。

所以,我们概括性的总结为四个名词:服务、产品、项目、外包。

3.1、服务

SaaS服务是一种理想形态,它采用租用的方式,靠服务订阅而活。由于数据是集中存放的,在大数据的加持下会有更大的想象空间。

SaaS行业比较像2C的互联网,以量取胜,通常不会服务于非常大的客户—那是他们的累赘。

技术属性的服务,比如云服务、直播基础服务;业务属性的服务,比如基于微信的营销平台,都可以做成SaaS服务。

但由于它本质上还是2C互联网的打法,又有利润的诉求,建设起来就有很多阻力。再加上乙方的信任,往往要有关键事件的促进,往往建设时间都比较长。

由于对客户的培养成本高昂,单位获客成本会更加高昂。除了运营理念的跟进,也会有中间代理商参与到市场的推广中来。但一旦企业采购并推广了服务,短时间内不会再做轻易的迁移,我们可以认为获取了相对稳定的客户。

个人认为,经过短暂的烧钱之后,如果不能实现快速盈利,以及盈利模式的考验,大概率会以失败告终。当然有主动烧钱的金主另当别论。

3.2、产品

另一个比较舒服的模式就是产品。比如SAP,滋润的活了这么多年。但是做一个好的产品是非常困难的,它通常由项目演变而来。

做一些通用的产品,如CRM、供应链、财务等,会陷入到激烈的竞争中来。这些软件的操作体验和价格都相差无几,在产品和业务形态上不会有特别大的创新。

行业应用产品,会有非常大的行业壁垒。通过方法论抽象出来的产品+流程,整体打包后,通常不能直接售卖。多数客户希望获取个性化的自定义功能。

所以,产品的设计要伸缩有度。核心业务要保持绝对的话语权,在一些边缘业务和展现形态上,做到良好的扩展性。就像是,所有的人都会吃饭喝水,但穿的衣服却不尽相同。

无法做到这一点,产品就会沦为项目。产品属于一直做项目后归纳、抽象的一套业务引擎+数据引擎,属于乙方的叛变。

3.3、项目

服务或者产品沦化为项目,我们称作产品的崩坏。是一种与熵增相违背的演进方式。

产品有两种形式的演进。一种是自上而下,先定义产品形态,然后进行市场培养,这需要极高的业内知识沉淀;一种是自下而上,先做项目,甚至外包,然后在此基础上抽象出通用版本。

项目就属于后者。通常是企业没有能力提供通用的产品,或者没有能力在市场上对客户进行培养,而选择的一种折衷方案。

有的企业,会对服务类产品形态心存疑虑,认为数据都存放在别人的平台上,不安全,所以也会采用项目形式。

做项目非常苦逼,碰到阴晴不定的甲方最为痛苦。项目有可能是远程办公,也可能是驻场项目,受到甲方的文化和流程约束。

经过销售达成合同,项目即可开始。合同通常不会明确写明系统的功能需求,系统边界不明,造成后期的沟通成本大,功能频繁变动。

做项目和做产品一样,工期会特别长,持续3-5年也是可能的,技术的领先不再是主要的考量点。

3.4、外包

外包是B端市场最主要的形式,占有的市场份额也是最大的。从互联网诞生开始,外包就一直存在,到今天依然坚挺。

企业的数字化,需要用人,但大多数都是临时性的。如果企业不想出这部分长期养人的开支,就会产生外包。

所以,外包几乎无孔不入,任何行业都充斥着它们的身影。这里充斥着数量巨大的伪业务,将开发人员当作工具使用。

外包定制模式高(客户上帝),服务半径小(功能有限),对开发人员的要求也是一般,利润也来自于销售和支出之间的差价上。

有些甲方非常过分,会极力的压榨外包的利润,有的甚至会对乙方的管理和人工成本插上一脚。

4、挑战

在2B的企业,面临的挑战不是故事、营销、技术,而变成了产品、管理、成本。在不同层次的B端企业,这三者的重要程度会有不同的侧重点。

但它们有一个统一的外部威胁,就是互联网的集权化。

4.1、集权化

这已经不是老生常谈的问题。拥有流量优势、技术优势、运营优势的大厂,更容易占取利润更大的业务。剩下的又脏又臭的业务,被创业者和外包分而食之。

从最近某大企业频繁的控评就可以看出,垄断对社会、对公民,都是伤害非常大的。

更可憎的是资金优势。通过巨大的资金池,在过去风口受益的,拥有巨大现金流的公司,会通过收购和投资的模式,强行切入到某个行业。

比如阿里,它的触角已经深到了以前互联网不屑入围的政企项目。通过资源拿到项目,然后通过外包完成项目,赚取差价。

今天广大的创业者,如果再盲目的追随大厂的脚步,就只能拿一点残羹冷炙,无异于自掘坟墓。

4.2、管理挑战

做2B业务,尤其是项目和外包,员工的幸福感,会非常低。第一,客户需求的频繁更改,会造成工期变长,加班频繁;第二,对功能没有话语权,不容易有成就感;第三,在薪资报酬和个人发展方面,没有想象空间。

另外,因公共产品抽离不及时,或者基础技术不牢固,在经营范围壮大的时候,会遇到开发人员的膨胀。为了应对项目,通常会靠堆人头的方式,引入很多初级人员。这部分人员对技术和业务并不熟悉,并不能很快参与到项目中来。同时,初级人员对自己螺丝钉的定位通常会感到沮丧,明确的晋升通道是必须的。

底层的基础设施不能共用,管理成本反而增加,是属于1+1小于2的情况。

我一般都对快速扩张的企业抱有较大的怀疑,因为接下来很大的可能,会面临裁员,甚至倒闭。

管理不到位,或者各层级研发人员的断层,都会造成员工的幸福感降低,造成巨大的流动性。

4.3、成本挑战

2B的服务、项目、外包,很多利润都是透明的,想象空间极为有限。由于竞争的因素,设施建设齐全的公司,会采用薄利多销的方式,售卖出去,造成了利润的进一步降低。

有两种形式可以突围,以使利润逐步增大。

第一,是极度压缩投入成本,包括人工成本、研发成本。这通常是和管理相悖的,充满了更大的挑战。如何在一个没有饼的空间中强行画饼,并画的有模有样,是值得深入思考的。

第二,是从外包、项目模式,逐渐进化为产品、服务模式。当在行业中树立了影响力,并有着竞争力强的产品体系,会让客户严重依赖。以通吃的模式,扩大服务半径,挤占其他竞争公司的空间。当达到这一程度,谓之为独角兽。

4.4、观念挑战

2B行业充斥着无数的销售和中间商,即使这样,产品的传播能力也非常有限。被服务的企业,大多数希望通过一次采购,即可获取所有的功能,做所谓的一锤子买卖。这在软件的演化周期上,并不合理。

一锤子买卖会卖个好价钱,但是不常有,通常会造成企业的盲目自大,认清不到自己的本事,以至于盲目扩张。发展成租用的模式,可获取稳定可控的现金流,不至于在销售空档期青黄不接。

通过push的方式获取客户,简单粗暴,但规模有限。如何让客户主动找到自己,打造行业的影响力,还需要借助2C的一些打法:互联网侧重于运营,来替代传统的销售,覆盖面更为广泛。通过第三方渠道了解到你,和你主动对人推销,在情感上的信任程度也是不一样的。

所以搞2B,不是说就把2C全部给扔了,反之亦然。随着发展,熵增会蔓延至行业的方方面面,直到所有的价值洼地都被填平,变成充满了独角兽的一汪死水。

4.5、不要被伪业务给耽误了

所谓的业务,也有真业务和伪业务之分。真正的业务,是能够结合行业规范和流程,持续的、广泛的产生服务价值的需求;伪业务,是指非常的个性化,行业内不予承认,或者一些天马行空的探索性需求。

这两种业务有着天囊之别,伪业务在验证它的无用之后,你的积累和经验,将随着它的破灭一无所有。白白浪费青春在这些无用的需求之上。

从业者要警惕伪业务,即使你是一个乙方的公司,能够从中获取利润。这些伪业务知识体系不能够长期积累,会让企业一直处于疲于奔命的状态。

End

曾经一片蓝海的2C互联网,如今一片死寂。大公司凭借自身优势,像螃蟹一样横行无阻,2C已经变为头部公司的流量争夺战。你方唱罢我登场,不断拉锯。

新技术是搅局者,但除非是变革性的,搅不出太大风浪。

越来越多的公司关注2B,很多已经在2B领域布局了很多年。但由于市场实在太大,没有公司能够一口吞下它。行业垂直细分下来,也没有公司能够承受这样的人力成本。

在2B这片混乱之地,归于沉寂之前,借鉴2C的一些精华,融入到商业体系中来,或许会有不一样的效果。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

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

推荐一个高仿微信的开源项目,有点屌!

发表于 2021-03-29

前言

该项目是一款高仿微信的开源项目,iOSAppTemplate代码重构,基于TLKit、 ZZFLEX实现。

已实现的功能

1. 消息界面

  • 消息列表(新会话加入,DB)
  • 消息侧滑删除
  • 好友搜索(支持模糊查询)
  • 更多菜单(可动态定制items)

2. 通讯录界面

  • 好友列表(分组算法、DB)
  • 好友搜索
  • 好友资料(UI抽象模板),资料设置UI(使用设置类UI模板)
  • 新的朋友(读取手机联系人信息)
  • 群聊(UI,DB)
  • 标签(UI,逻辑)

3. 发现界面(使用菜单类UI模板)

  • 好友圈(整体架构,部分UI)
  • 扫一扫(UI,二维码扫描,条形码扫描)
  • 摇一摇UI
  • 漂流瓶UI
  • 购物、游戏(封装WebView)

4. 我界面(使用菜单类UI模板)

  • 个人信息(使用设置类UI模板)
  • 表情(UI、网络请求、下载、管理)
  • 设置(抽象设置类UI通用模板)
  1. 字体大小
  2. 聊天背景
  3. 我的表情
  4. 清空聊天记录

5. 聊天界面

  • 聊天输入框
  • 消息展示视图
  1. 文字消息
  2. 图片消息
  3. 表情消息
  4. 语音消息
  • 聊天键盘
  1. 表情键盘(动态增删表情包)
  2. 更多键盘
  • 聊天记录存储(DB)

第三方库

  • Masonry:自动布局框架,简洁高效
  • FMDB:sqlite数据库管理框架
  • AFNetworking:网络请求
  • SDWebImage:网络图片下载、缓存
  • MJExtension:JSON - Model互转框架,高效低耦合
  • MJRefresh:下拉刷新,上拉加载更多,继承简单
  • CocoaLumberjack:日志分级、本地化
  • MWPhotoBrowser:图片选择器
  • SVProgressHUD:进度提示框

效果展示

首页

通讯录

聊天

朋友圈

发现

最后

该项目目前已经开源。作者也在对功能进行完成,后续包括视频消息、地理位置消息、好友点赞和回复,聊天模块抽离也在实现中。如果你觉得该项目不错,自己有能力的话,也可以去项目下贡献些自己的力量。

项目地址

Gitee:gitee.com/edceezyy/TL…

本文转载自: 掘金

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

深度好文!RocketMQ高级进阶知识精讲!

发表于 2021-03-29

前言

大家好,我是jack xu,本文是RockeMQ精讲系列的最后一篇,讲的是RockeMQ一些进阶高级的知识,在我们平时的面试中会用到,掌握了这些东西也是体现一个高手和crud boy的区别。

第一讲:《RocketMQ高可用架构及二主二从异步集群部署》

第二讲:《RocketMQ扫盲贴及Java API使用精讲》

本文使用到的源码:github.com/xuhaoj/rock…

官方文档的翻译:www.itmuch.com/books/rocke…

为了使大家能够清晰明了,有层次的掌握这些知识,我们从生产者、Broker、消费者三个维度来讲解。

生产者

消息发送规则

在RocketMQ中,是基于多个Message Queue来实现类似于kafka的分区效果。如果一个Topic要发送和接收的数据量非常大,需要能支持增加并行处理的机器来提高处理速度,这时候一个Topic可以根据需求设置一个或多个Message Queue。Topic有了多个Message Queue 后,消息可以并行地向各个Message Queue发送,消费者也可以并行地从多个Message Queue读取消息并消费。

那么一个消息会发送到哪个Message Queue上呢,这个就需要我们的路由分发策略了。在Send的众多重载方法中,有这样一个参数
MessageQueueSelector。
image.png
RocketMQ中已经帮我们实现了三个实现类:

  • SelectMessageQueueByHash(默认):它是一种不断自增、轮询的方式。
  • SelectMessageQueueByRandom:随机选择一个队列。
  • SelectMessageQueueByMachineRoom:返回空,没有实现。
    如果上面这几个不能满足我们的需求,还可以自定义MessageQueueSelector,作为参数传进去:
1
2
3
4
5
6
7
8
java复制代码SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);

源码在example/ordermessage/Producer.java

顺序消息

一道很经典的面试题,如何保证消息的有序性?思路是,需要保证顺序的消息要发送到同一个message queue中。其次,一个message queue只能被一个消费者消费,这点是由消息队列的分配机制来保证的。最后,一个消费者内部对一个mq的消费要保证是有序的。我们要做到生产者 - message queue - 消费者之间是一对一对一的关系。

具体操作过程如下:

  1. 生产者发送消息的时候,到达Broker应该是有序的。所以对于生产者,不能使用多线程异步发送,而是顺序发送。
  2. 写入Broker的时候,应该是顺序写入的。也就是相同主题的消息应该集中写入,选择同一个Message Queue,而不是分散写入。
    要达到这个效果很简单,只需要我们在发送的时候传入相同的hashKey,就会选择同一个队列。

image.png
3. 消费者消费的时候只能有一个线程,否则由于消费的速率不同,有可能出现记录到数据库的时候无序。
在Spring Boot中,consumeMode设置为ORDERLY,在Java API中,传入MessageListenerOrderly的实现类即可。

1
java复制代码consumer.registerMessageListener(new MessageListenerOrderly() {

当然顺序消费会带来一些问题:

  1. 遇到消息失败的消息,无法跳过,当前队列消费暂停
  2. 降低了消息处理的性能

事务消息

分布式事务有很多种解决方案,其中一种就是使用RocketMQ的事务消息来达到最终一致性。下面我们来看下RocketMQ是怎么实现的。下面是RocketMQ官网的一张流程图,我们对照着图来分析讲解一下。
rocketmq.apache.org/rocketmq/th…

image.png

  1. 生产者向RocketMQ服务端发送半消息,什么叫半消息呢,就是暂不能投递消费者的消息,发送方已经将消息成功发送到了MQ服务端,此时消息被标记为暂不能投递状态,需要等待生产者对该消息的二次确认。
  2. MQ服务端给生产者发送ack,告诉生产者半消息已经成功收到了。
  3. 发送方开始执行本地数据库事务的逻辑。
  4. 执行完成以后将结果告诉MQ服务端,本地事务执行成功就告诉commint,MQ Server收到commit后则将半消息状态置为可投递,consumer最终将收到该消息;本地事务执行失败则发送rollback,MQ Server收到rollback以后则删除半消息,订阅费将不会收到该条消息。
  5. 未收到第4步的确认信息时,回查事务状态。消息回查: 因为网络闪断、生产者重启等原因,RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功。
  6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  7. 发送方根据检查本地事务的最终状态再次提交二次确认,发送commit或者rollback。
    上述就是整个事务消息的执行流程,下面我们来看下如何在代码中操作。
    RocketMQ中提供了一个TransactionListener接口,我们需要实现它,然后在executeLocalTransaction方法中实现执行本地事务逻辑。
1
2
3
4
5
6
java复制代码    @Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//local transaction process,return rollback,commit or unknow
log.info("executeLocalTransaction:"+JSON.toJSONString(msg));
return LocalTransactionState.UNKNOW;
}

这个方法必须返回一个状态,rollback,commit或者unknow,返回unknow之后,因为不确定到底事务有没有成功,Broker会主动发起对事务执行结果的查询,所以还要再实现一个checkLocalTransaction回查方法。

1
2
3
4
5
java复制代码    @Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
log.info("checkLocalTransaction:"+JSON.toJSONString(msg));
return LocalTransactionState.COMMIT_MESSAGE;
}

默认回查总次数是15次,第一次回查的间隔是6s,后续每次间隔60s。最后在生产者发送的时候指定下事务监听器即可。
image.png
源码在example/transaction/TransactionProducer.java

延迟消息

很多时候,我们村会在这样的业务场景:在一段时间之后,完成一个工作任务的需求,例如:滴滴打车订单完成之后,如果用户一直不评价,48小时会将自动评价为5星;外卖下单30分钟不支付自动取消等等。这种问题的解决方案有很多种,其中一种就是用RocketMQ的延迟队列来实现,但是开源版本功能被阉割了,只能支持特定等级的消息,商业版可以任意指定时间。

1
java复制代码   msg.setDelayTimeLevel(2); // 5秒钟

比如leve=2代表5秒,一共支持18个等级,延迟的级别配置在代码MessageStoreConfig中:

1
java复制代码  private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

Spring Boot中这样使用

1
java复制代码  rocketMQTemplate.syncSend(topic,message,1000,2);// 5秒钟

源码在example/delay/DelayProducer.java

Broker

物理存储

我们进入到RocketMQ存储的文件夹看一下,这个目录是我们在安装的时候指定的。

image.png
下面依次介绍下这几个文件夹的作用:

  1. checkpoint:文件检查点,存储commitlog、consumequeue、indexfile最后一次刷盘时间或时间戳。
  2. commitlog:消息存储目录,一个文件集合,每个默认文件1G大小,当第一个文件写满了,第二个文件会以初始量命名。比如起始偏移量是1073741824,第二个文件名为00000000001073741824,以此类推。

image.png

  1. config:运行时的配置信息,包含主题消息过滤信息、集群消费模式消息消费进度、延迟消息队列拉取进度、消息消费组配置信息、topic配置属性等。
  2. consumequeue:消息消费队列存储目录,我们可以看到在consumequeue文件夹下是按topic的名字建文件夹,在每一个topic下面又是按message queue的编号建文件夹,在每个message queue文件夹下就是存放消息在commit log的偏移量以及大小和Tag属性。

image.png
5. index:消息索引文件存储目录,在前面使用java api发送消息的时候,我们看到会传入一个keys的参数,它是用来检索消息的。所以如果出现keys,服务端就会创建索引文件,以空格分割的每个关键字都会产生一个索引。单个IndexFile可以保存2000W个索引,文件固定大小约为400M。索引使用的是哈希索引,所以key尽量设置为唯一不重复。

存储理念

我们来看下RocketMQ官网的说明,rocketmq.apache.org/rocketmq/ho… ,我们来导读一下,首先是说kafka为什么不能支持更多的分区,然后说在RocketMQ中我们是如何支持更多分区的。

image.png

  1. 每个分区存储整个消息数据。虽然每个分区被有序地写入磁盘,但随着并发写入分区数量的增加,从操作系统的角度来看,写入变得随机。
  2. 由于数据文件分散,难以使用Linux IO Group Commit机制。

所以RocketMQ干脆另辟蹊径,设计了一种新的文件文件存储方式,就是所有topic的所有消息全部写在同一个文件中,这样就能够保证绝对的顺序写。当然消费的时候就复杂了,要到一个巨大的commitlog中去查找消息,我们不可能遍历所有消息吧,这样效率太慢了。

那怎么办呢?这个就是上面提到的consume queue,它把consume group消费的topic的最后消费到的offset存储在里面。当我们消费的时候,先从consume queue读取持久化消息的起始物理位置偏移量offset、大小size和消息tag的hashcode值,随后再从commitlog中进行读取待拉取消费消息的真正实体内容部分。

consume queue可以理解为消息的索引,它里面没有消息,当然这样的存储理念也不是十全十美,对于commitlog来说,写的时候虽然是顺序写,但是读的时候却变成了完全的随机读;读一条消息先会读consume queue,再读commit log,这样增加了开销。

文件清理策略

跟kalka一样,commit log的内容在消费之后是不会删除,这样做有两个好处,一个是可以被多个consumer group重复消费,只要修改consumer group,就可以从头开始消费,每个consumer group维护自己的offset;另一个是支持消息回溯,随时可以搜索。

但是如果不清理文件的话,文件数量不断地增加,最终会导致磁盘可用空间越来越少,所以RocketMQ会将commitLog、consume queue这些过期文件进行删除,默认是超过72个小时的文件。这里会启动两个线程去跑。

1
2
3
4
java复制代码    private void cleanFilesPeriodically() {
this.cleanCommitLogService.run();
this.cleanConsumeQueueService.run();
}

过期文件选出来以后,什么时候去清理呢,有两种情况。一种是通过定时任务,每天凌晨四点去删除这些文件。第二种是磁盘使用空间超过75% 了,这时候已经火烧眉毛了,我还等到你四点干嘛,立即马上就清理了。

如果情况更严重,如果磁盘空间使用率超过85%,会开始批量清理文件,不管有没有过期,直到空间充足;如果磁盘使用率超过90%,会拒绝消息写入。

零拷贝

大家都知道RocketMQ的消息是存储在磁盘上的,但是怎么还能做到这么低的延迟和这么高的吞吐量,其中的一个奥秘就是使用到了零拷贝技术。

首先和大家介绍一下page Cache的概念,这个是操作系统层面的,CPU如果要读取或者操作磁盘上的数据,必须要把磁盘的数据加载到内存中,这个加载的大小有一个固定的单位,叫做Page。x86的linux中一个标准的页大小是4kb。如果要提升磁盘的访问速度,或者说减少磁盘的IO,可以把访问过的Page在内存中缓存起来,这个内存的区域就叫做Page Cache。

下次处理IO请求的时候,先到Page Cache中查找,找到了就直接操作,没找到再到磁盘中去找。当然Page Cache本身也会对数据进行预读,对于每个文件的第一个读请求操作,系统也会将所请求的页的相邻后几个页一起读出来。但是这里还有个问题,我们知道虚拟内存分为内核空间和用户空间,Page Cache属于内核空间,用户空间访问不了,还需要从内核空间拷贝到用户空间缓冲区,这个copy的过程就降低了数据访问的速度。

为了解决这个问题,就产生了零拷贝技术,干脆把Page Cache的数据在用户空间中做一个地址映射,这样用户进行就可以通过指针操作直接读写Page Cache,不再需要系统调用(例如read())和内存拷贝。RocketMQ中具体的实现是使用mmap(memory map,内存映射),而kafka用的是sendfile。
image.png

消费者

消费端的负载均衡与rebalance

和kafka一样,消费端也会针对Message Queue做负载均衡,使得每个消费者能够合理的消费多个分区的消息。消费者挂了,消费者增加,这时候就会用到我们的rebalance。

在RebalanceImpl.class的277行有rebalance的策略

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码      AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),e);
return;
}

AllocateMessageQueueStrategy有6种实现的策略,也可以自定义实现,在消费者端指定即可。

1
java复制代码consumer.setAllocateMessageQueueStrategy();
  • AllocateMessageQueueAveragely:平均分配算法(默认)

image.png

  • AllocateMessageQueueAveragelyByCircle:环状分配消息队列

image.png

  • AllocateMessageQueueByConfig:按照配置来分配队列,根据用户指定的配置来进行负载
  • AllocateMessageQueueByMachineRoom:按照指定机房来配置队列
  • AllocateMachineRoomNearby:按照就近机房来配置队列
  • AllocateMessageQueueConsistentHash:一致性hash,根据消费者的cid进行
  • 队列的数量尽量要大于消费者的数量。*

重试与死信队列

在消费者端如果出现异常,比如数据库不可用、网络出现问题、中途断电等等,这时候返回给Broker的是RECONSUME_LATER,表示稍后重试。这个时候消息会发回到Broker,进入到RocketMQ的重试队列中。服务端会为consumer group创建一个名字为%RETRY%开头的重试队列。

image.png
重试队列过一段时间后再次投递到这个ConsumerGroup,如果还是异常,会再次进入到重试队列。重试的时间间隔会不断衰减,从10秒开始直到2个小时:10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,最多重试16次。

而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列。Broker会创建一个死信队列,死信队列的名字是%DLQ%+ConsumerGroupName,应用可以监控死信队列来做人工干预。一般情况下我们在实际生产中是不需要重试16次,这样既浪费时间又浪费性能,理论上当尝试重复次数达到我们想要的结果时如果还是消费失败,那么我们需要将对应的消息进行记录,并且结束重复尝试。

源码在jackxu/SimpleConsumer.java

MQ选型分析

下面列出市面上常见的三种MQ的分析对比,供大家在项目中实际使用的时候参考对比:
image.png
好,RocketMQ系列到这里就结束了,感谢大家的观看~

本文转载自: 掘金

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

MyBatis 三种批量插入方式的比较,我推荐第 3 个!

发表于 2021-03-29

公众号:Java小咖秀,网站:javaxks.com

作者:楼主楼主 , 链接: www.jianshu.com/p/cce617be9…

数据库使用的是 sqlserver,JDK 版本 1.8,运行在 SpringBoot 环境下
对比 3 种可用的方式

  1. 反复执行单条插入语句
  2. xml 拼接 sql
  3. 批处理执行

先说结论:少量插入请使用反复插入单条数据,方便。数量较多请使用批处理方式。(可以考虑以有需求的插入数据量 20 条左右为界吧,在我的测试和数据库环境下耗时都是百毫秒级的,方便最重要)。无论何时都不用 xml 拼接 sql 的方式。

代码

拼接 SQL 的 xml
newId() 是 sqlserver 生成 UUID 的函数,与本文内容无关

1
2
3
4
5
6
7
8
ini复制代码<insert parameterType="java.util.List">
INSERT INTO tb_item VALUES
<foreach collection="list" item="item" index="index" separator=",">
(newId(),#{item.uniqueCode},#{item.projectId},#{item.name},#{item.type},#{item.packageUnique},
#{item.isPackage},#{item.factoryId},#{item.projectName},#{item.spec},#{item.length},#{item.weight},
#{item.material},#{item.setupPosition},#{item.areaPosition},#{item.bottomHeight},#{item.topHeight},
#{item.serialNumber},#{item.createTime}</foreach>
</insert>

Mapper 接口
Mapper 是 mybatis 插件 tk.Mapper 的接口,与本文内容关系不大

1
2
3
csharp复制代码public interface ItemMapper extends Mapper<Item> {
int insertByBatch(List<Item> itemList);
}

Service 类

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
scss复制代码@Service
public class ItemService {
@Autowired
private ItemMapper itemMapper;
@Autowired
private SqlSessionFactory sqlSessionFactory;
//批处理
@Transactional
public void add(List<Item> itemList) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
ItemMapper mapper = session.getMapper(ItemMapper.class);
for (int i = 0; i < itemList.size(); i++) {
mapper.insertSelective(itemList.get(i));
if(i%1000==999){//每1000条提交一次防止内存溢出
session.commit();
session.clearCache();
}
}
session.commit();
session.clearCache();
}
//拼接sql
@Transactional
public void add1(List<Item> itemList) {
itemList.insertByBatch(itemMapper::insertSelective);
}
//循环插入
@Transactional
public void add2(List<Item> itemList) {
itemList.forEach(itemMapper::insertSelective);
}
}

测试类

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
swift复制代码@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ApplicationBoot.class)
public class ItemServiceTest {
@Autowired
ItemService itemService;

private List<Item> itemList = new ArrayList<>();
//生成测试List
@Before
public void createList(){
String json ="{\n" +
" \"areaPosition\": \"TEST\",\n" +
" \"bottomHeight\": 5,\n" +
" \"factoryId\": \"0\",\n" +
" \"length\": 233.233,\n" +
" \"material\": \"Q345B\",\n" +
" \"name\": \"TEST\",\n" +
" \"package\": false,\n" +
" \"packageUnique\": \"45f8a0ba0bf048839df85f32ebe5bb81\",\n" +
" \"projectId\": \"094b5eb5e0384bb1aaa822880a428b6d\",\n" +
" \"projectName\": \"项目_TEST1\",\n" +
" \"serialNumber\": \"1/2\",\n" +
" \"setupPosition\": \"1B柱\",\n" +
" \"spec\": \"200X200X200\",\n" +
" \"topHeight\": 10,\n" +
" \"type\": \"Steel\",\n" +
" \"uniqueCode\": \"12344312\",\n" +
" \"weight\": 100\n" +
" }";
Item test1 = JSON.parseObject(json,Item.class);
test1.setCreateTime(new Date());
for (int i = 0; i < 1000; i++) {//测试会修改此数量
itemList.add(test1);
}
}
//批处理
@Test
@Transactional
public void tesInsert() {
itemService.add(itemList);
}
//拼接字符串
@Test
@Transactional
public void testInsert1(){
itemService.add1(itemList);
}
//循环插入
@Test
@Transactional
public void testInsert2(){
itemService.add2(itemList);
}
}

测试结果:

10 条 25 条数据插入经多次测试,波动性较大,但基本都在百毫秒级别

方式 50 条 100 条 500 条 1000 条
批处理 159ms 208ms 305ms 432ms
xml 拼接 sql 208ms 232ms 报错 报错
反复单条插入 1013ms 2266ms 8141ms 18861ms

其中 拼接 sql 方式在插入 500 条和 1000 条时报错(似乎是因为 sql 语句过长,此条跟数据库类型有关,未做其他数据库的测试):
com.microsoft.sqlserver.jdbc.SQLServerException: 传入的表格格式数据流(TDS)远程过程调用(RPC)协议流不正确。此 RPC 请求中提供了过多的参数。最多应为 2100

可以发现

  • 循环插入的时间复杂度是 O(n), 并且常数 C 很大
  • 拼接 SQL 插入的时间复杂度(应该)是 O(logn), 但是成功完成次数不多,不确定
  • 批处理的效率的时间复杂度是 O(logn), 并且常数 C 也比较小

结论

循环插入单条数据虽然效率极低,但是代码量极少,在使用 tk.Mapper 的插件情况下,仅需代码,:

1
2
3
4
typescript复制代码@Transactional
public void add1(List<Item> itemList) {
itemList.forEach(itemMapper::insertSelective);
}

因此,在需求插入数据数量不多的情况下肯定用它了。

xml 拼接 sql 是最不推荐的方式,使用时有大段的 xml 和 sql 语句要写,很容易出错,工作效率很低。更关键点是,虽然效率尚可,但是真正需要效率的时候你挂了,要你何用?

批处理执行是有大数据量插入时推荐的做法,使用起来也比较方便。

本文转载自: 掘金

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

从分布式事务解决到Seata使用,一梭子给你整明白了

发表于 2021-03-29

大家好,欢迎来到小菜同学的个人 solo 学堂,知识免费,不吝吸收!关注免费,不吝动手!

本文主要介绍 分布式事务

如有需要,可以参考

如有帮助,不忘 点赞 ❥

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

生活可能对你耍无赖,但科技不能

我去小卖部买东西,付完了钱,老板转身抽了口烟,却忘记了我付完钱?这种情况怎么办,发生在日常生活并不奇怪。但是你在网上下单,付完了钱,刚要查看订单,却提示你待支付,心中几万只草泥马跑过也不得而知!所以防止这种情况的发生,分布式事务也变得尤为重要。

有人纳闷了,付不付钱跟分布式事务有什么关系,这不是程序耍无赖吗?但是耍无赖的背后却是因为分布式事务在作祟!如果还不明白,那可能你还没明白什么是事务,什么是分布式事务~

分布式事务

定义

事务提供一种机制将一个活动涉及的所有操作都纳入到一个不可分割的执行单元,组成事务的祝所有操作只有在操作均正常执行的情况下才能提交,只要其中任一操作执行失败,都会导致整个事务的回滚。简单来说,要么做,要么不做。

听起来有点 man,不要迷恋,先来了解一下事务的四大特性:ACID

  • A(Atomic):原子性,构成事物的所有操作,要么全部执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
  • C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。一旦所有事务动作完成,事务就被提交,数据和资源处于一种满足业务规则的一致性状态中。比如上面说的,我向商店老板付了钱,我这边扣除了100,而老板增加了100,这种就称为一致性。
  • I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务互不干扰,一个事务不能看到其他事务运行过程的中间状态,通过配置事务隔离级别可以避免在脏读、幻读、不可重复读等问题。
  • D(Durability):持久性,事务完成之后,该事务对数据的更改会持久化到数据库中,并且不会被回滚
单体事务

早期我们使用的还是单体架构,像是一个大家族,其乐融融的生活在一起日夜耕作。

时间久了,各种各样的问题自然而然的也出现了:复杂性高,部署频率低,可靠性差,扩展能力受限... 承受了太多不该承受的流言蜚语,而大家也逐渐找寻新的出路, 那微服务架构也便受应出现:易于开发、扩展、理解和维护,不会受限于任何技术栈,易于和第三方应用系统集成... 太多太多的优点,让单体系统也逐渐淡出人们的视角,好像如果现在不用微服务架构开发项目,就与社会脱节了~ 好处很多,但是问题也会变得更加复杂。这节我们不讲别的,就来看看分布式事务是咋回事。

事务无论在单体还是微服务中都肯定是存在,但是在 单体 架构中,我们通常是怎么解决事务的呢? @Transactional,单靠这个注解就可以开启事务来保证整个操作的 原子性。

分布式事务

微服务架构,其实就是将传统的单体拆分成多个服务,然后多个服务之间相互配合,来完成业务需求。

分布式事务就是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

既然说到分布式事务了,我们不妨一起了解一下微服务中的 CAP理论

  • C(Consistency):一致性。服务A、B、C三个节点都存储了用户数据,三个节点的数据都需要保持同一时刻数据一致性
  • A(Availability):可用性。服务A、B、C三个节点,其中一个节点如果宕机了,不能影响整个集群对外提供服务。
  • P(Partition Tolerance):分区容错性就是允许系统通过网络协同工作,分区容错性要解决由于网络分区导致数据的不完整及无法访问等问题。

我们都知道鱼和熊掌不可兼得,三者不能兼备择两者是也!CAP 目前来说无法都兼备,因此当前微服务策略中要么 CA,要么CP,不然就是AP。而这个时候又有一个理论出现了,那就是 BASE理论 。它是用来对 CAP理论 进行一些补充,它值得是:

  • BA(Basically Available):基本可用
  • S(Soft State):软状态
  • E(Eventually Consistent):最终一致性

这个理论的核心思想便是:如果我们如法做到强一致性,那么每个应用都应该根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

出现场景

让我们回到分布式事务中来,什么时候会出现分布式事务呢?

场景1: 虽然时单体的架构服务,但由于在分库的情况下,依然会导致分布式事务的情况,因此单体服务不会出现分布式事务的这种说法,破~

场景2: 分布式架构下,两个服务之间相互调用,虽然使用的是同一个数据库,但是还是会出现分布式事务。谁让你使用的是分布式架构呢~

场景3: 分布式架构下,两个服务之间相互调用,使用的是不用的数据库,这种情况下肯定会出现分布式事务的问题,想都不用想!

解决方法

有问题的地方便会有方法,当然也不一定,但是在这里,分布式事务问题确实有解决的方法, 如果没有,小菜也不会写这篇文章来自讨苦吃了!

方法一:全局事务

不知道这里该说 全局事务 会让你比较熟悉,还是 两阶段提交(2PC) 会让你比较熟悉,还是说都不熟悉~,不熟悉也没关系,小菜带你熟悉熟悉!

全局事务是基于DTP模型实现的,它规定了要实现分布式事务需要三种角色:

  • AP(Application):应用系统(微服务)
  • TM(Transaction Manager):事务管理器(全局事务管理)
  • RM(Resource Manager):资源管理器(数据库)

除了 AP 这个角色,我们多认识了其他两个同学分别是事务管理器和资源管理器,那么他们起到什么作用呢,那我们就得看,这个两阶段提交是哪两阶段了!

阶段1 :表决阶段

所有参与者都将自己的事务进行预提交,并将能否成功的信息反馈给协调者

  1. 事务管理器发一个 prepare 指令给 A 和 B 两个服务器
  2. A 和 B 两个服务器收到消息后,根据自身情况,判断自己是否可以提交事务
  3. 将处理结果记录到资源管理器中
  4. 将处理结果返回给事务管理器
阶段2 :执行阶段

协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚

  1. 事务管理器向 A 和 B 两个服务器发送提交指令
  2. A 和 B 两个服务器收到指令后,将自己本身事务提交
  3. 将处理结果记录到资源管理器
  4. 将处理结果返回给事务管理器

这就是两阶段提交的大致过程,它提高了数据一致性的概率,实现成本较低。但是这种实现方式带来的缺点也是很明显的!

  • 单点故障:如果事务管理器出现了故障,整个系统将不可用
  • 同步阻塞:延迟了提交事件,加长了资源阻塞事件,不适合高并发的场景
  • 数据不一致:如果执行到第二阶段,依然存在commit结果未知的情况,只有部分参与者接收到 commit 消息,部分没有收到,那也只有部分参与者提交了事务,依然会导致数据不一致问题
方法二:三阶段提交

既然两阶段提交解决不了问题,那我们就来三阶段提交。三阶段提交相对于两阶段提交来说增加了 ConCommit 阶段和超时机制。在一段规定时间内,如果服务器参与者没有接受到来自事务管理器的提交执行,那他们就会自己自动提交,这样子就能解决两阶段中单体故障问题。

我们来看看三阶段提交是哪三阶段:

  • CanCommit:准备阶段。这个阶段要做的事就和两阶段提交一样,先去询问参与者是否有条件接收这个事务,这样子不会太暴力,一开始就直接干活锁死资源。
  • PreCommit:这个阶段是事务管理器向各个参加者发送准备提交请求,各个参与者接到请求或,将处理结果记录到自己的资源管理器中,如果准备好了,就会想协调者反馈ACK表示我已经准备好提交了。
  • DoCommit:这个就断就是从 预提交状态 转为 提交状态。事务管理器向各个参与者发送 提交 请求,参与者接收到请求后,就会各自执行自己事务的提交操作。将处理结果记录到自己的资源管理器中,并向协调者反馈 ACK 表示自己已经完成事务,如果有一个参与者未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

其实三阶段提交看起来就是把两阶段提交中的提交阶段变成了 预提交阶段 和 提交阶段。

那其实从上面可以看到,三阶段提交解决的只是两阶段提交中 单体故障 的问题,因为加入了超时机制,这里的超时的机制作用于 预提交阶段 和 提交阶段。如果等待 预提交请求 超时,那参与者相当于说啥都没干,直接回到准备阶段之前。如果等到提交请求超时,那参与者就会提交事务了。

所以可以看到其实 三阶段提交还是没根本解决问题,虽然比两阶段提交进步了一点点~

方法三:TCC

TCC(Try Confirm Cancel) ,它是属于补偿型分布式事务。它的核心思想是 针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC 实现分布式事务一共有三个步骤:

  • Try:尝试待执行的业务

这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的所有资源

  • Confirm:确认执行业务

确认执行业务的操作,不做任何业务检查,只使用Try阶段预留的业务资源。通常情况下,采用TCC则会认为 Confirm 阶段是不会出错的。只要 Try 成功,则 Confirm 一定成功。如果 Confirm 出错了,则需要引入重试机制或人工处理

  • Cancel:取消待执行的业务

取消 Try 阶段预留的业务资源。通常情况下,采用 TCC 则认为 Cancel 阶段也是一定能成功的,若 Cancel 阶段真的出错了,也要引入重试机制或人工处理

TCC 是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。它的优缺点如下:

优点: 吧数据库层的二阶段提交上提到了应用层来实现,规避了数据库的 2PC 性能低下问题

缺点:TCC 的 Try、Confirm 和 Cancel 操作功能需业务提供,开发成本高。TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作

方法五:可靠消息事务

消息事务的原理是 将两个事务通过消息中间件来进行异步解耦。基于可靠消息服务的方案是通过消息中间件来保证上、下游应用数据操作的一致性。假设有 A、B两个服务,分布可以处理 A、B两个任务,此时需要存在一个业务流程,将任务 A和B 放到同一个事物中处理,这种方式就可以借助消息中间件来实现。

整体上可以分为两个大的步骤:A服务向消息中间件发布消息 和 消息向B服务投递消息

步骤一: A 服务向消息中间件发布消息

  1. 在服务A处理任务A前,首先向消息中间件发送一条半信息
  2. 消息中间件收到后将该消息持久化,但不进行投递。持久化成功后,向A服务返回确认应答
  3. 服务A收到确认应答后,便可以开始处理任务A
  4. 任务A处理完成后,服务A便会向消息中间件发送Commit 或者 Rollback 请求,该请求发送完成后,服务A的工作任务就结束了,该事务的处理过程也就结束了
  5. 在消息中间件收到 Commit 后,便会向 B 服务投递消息,如果收到 Rollback 便会直接丢弃消息

如果消息中间件在最后的过程中,长时间没有收到服务A 发送的 Commit 或 Rollback 指令,这个时候就需要依靠 超时询问机制

超时询问机制:

服务A除了实现正常的业务流程之外,还是需要提供一个可供消息中间件事务询问的接口。在消息中间件第一次收到消息后便会开始计时,如果超过规定的时间没有收到后续的指令,就会主动调用服务A提供的事务询问接口,询问当前服务的状态,通常来说该接口会返回三种结果,中间件需要根据这三种不同的结果做出不同的处理:

  • 提交:直接将该消息投递给服务B
  • 回滚:直接将该消息丢弃
  • 处理中:继续等待,重新计时

步骤二: 消息中间件向B服务投递消息

消息中间件收到A服务的提交 Commit指令后便会将该消息投递给B服务,然后将自己的状态置为阻塞等待状态。B服务收到消息中间件发送的消息后便开始处理任务B,处理完成后便会向消息中间件发出回应。但是在消息中间件阻塞等待的时候同样会出现问题

  • 正常情况:消息中间件投递完消息后,进入阻塞等待状态,在收到确认应答后便认为事务处理完成,该流程结束
  • 等待超时情况:在等待确认应答超时之后就会重新进行投递,直到B服务器返回消费成功响应为止。而消息重试的次数和时间间隔都可以设置,如果最终还是不能成功进行投递,则需要人工干预。

可靠消息服务方案是实现了 最终一致性。对比本地消息表实现方案,不需要再建立消息表。不用依赖本地数据库事务,适用于高并发的场景。RocketMQ 就很好的支持了消息事务。

方法四:最大努力通知

最大努力通知也成为定期校对,是对可靠消息服务的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游服务消费。

同样的这个跟消息事务一样可以分为两步:

步骤一: 服务A向消息中间件发送消息

  1. 在处理业务的同一个事务中,向本地消息表写入一条记录
  2. 消息发送者不断取出本地消息表中的消息发送到消息中间件,如果发送失败则进行重试

步骤二: 消息中间件向服务B投递消息

  1. 消息中间件收到消息后便会将消息投递到下游服务B,服务B收到消息后便会执行自己的业务
  2. 当服务B业务处理成功后,便会向消息中间件返回反馈应答,消息中间件便可将该消息删除,该流程结束
  3. 如果消息中间件向服务B投递消息失败,便会尝试重试,如果重试失败,便会将该消息接入失败消息表中
  4. 消息中间件同样需要提供查询失败消息的接口,服务B 定期查询失败信息,并进行消费

最大努力通知的方案实现比较简单,适用于一些最终一致性要求比较低的业务。

Seata

Seata概念

既然分布式事务处理起来这么麻烦,那能不能让分布式事务处理起来像本地事务那么简单。当然这是我们的愿景。当然这个愿景是所有开发人员所希望的。而阿里巴巴团队就为这个愿景做出了行动,发起了开源项目 Seata(Simple Extensible Autonomous Transaction Architecture) 。这是一套分布式事务解决方案,意在解决开发人员遇到的分布式事务各方面的难题。

Seata 的设计目标是对业务无侵入,因此它是从业务无侵入的两阶段提交(全局事务)着手,在传统的两阶段上进行改进,他把一个分布式事务理解成一个包含了若干分支事务的全局事务。而全局事务的职责是协调它管理的分支事务达成一致性,要么一起成功提交,要么一起失败回滚。也就是一荣俱荣一损俱损~

Seata 组成

我们看下 Seata 中存在几种重要角色:

  • TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
  • TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
  • RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。

这是一种很巧妙的设计,我们来看图:

执行流程是这样的:

  1. 服务A中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID
  2. 服务A中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
  3. 服务A开始执行分支事务
  4. 服务A开始远程调用B服务,此时 XID 会根据调用链传播
  5. 服务B中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
  6. 服务B开始执行分支事务
  7. 全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚
  8. TC 协调其管辖之下的所有分支事务,决定是提交还是回滚

Seata使用

我们从上面了解到了 Seata 的组成和执行流程,我们接下来就来实际的使用下 Seata。

示例演示

我们简单创建了一个微服务项目,其中有订单服务和库存服务。

我们这里采用了 nacos 作为注册中心,分别启动两个服务,我们在nacos控制台可以看到两个已经注册的服务:

号外:如果对nacos还不熟悉的小伙伴可以跳转查看 nacos讲解:微服务新秀之Nacos

我们接着创建了一个数据库,其中有两张表:c_order 和 c_product,其中商品表中有一条数据,而订单表中还未有数据,接下来我们将要对其进行操作!

我们现在模拟一个下单的过程:

  1. 请求进来,通过商品 pid 往数据库中查商品的信息
  2. 创建一条该商品的订单
  3. 对应扣减该商品的库存量
  4. 流程结束

我们接下来就进入代码演示一下:

注意:这里 ProductService 并非是库存服务里面的类,而是利用 Feign 远程调用库存服务的接口

代码三步走,正常请况下肯定是没有问题的:

订单生成,库存也对应减少,感觉自己代码可以上线进入正轨的时候,我们来模拟一下库存中的异常,库存商品数量归为 100,订单表清空:

我们继续发送下单请求,可以看到库存服务已经抛出了异常

正常来说这个时候,库存表数量不应该减少,订单表不应该插入订单数据,但是事实真的是这样的吗?我们看数据:

库存数量没减,但是订单却增加了。好了,到这里,你就已经见识到了分布式事务的灾难性危害。接下来主角登场!

Seata 安装

我们首先需要点击下载地址进行下载 Seata。

由于我们是使用 nacos 作为服务中心和配置中心,因此我们下载解压后需要做一些修改操作

  • 进入 conf 目录编辑 registry.conf 和 file.conf 两个文件,编辑后内容如下:

  • 由于新版 Seata 中没有 nacos-conf.sh 和 config.txt 两个文件,因此我们需要独立下载:

nacos-config.sh 下载地址

config.txt 下载地址

我们需要将 config.txt 文件放到 seata 目录下,而非 conf 目录下,并且需要修改 config.txt 内容

config.txt就是seata各种详细的配置,执行 nacos-config.sh 即可将这些配置导入到nacos,这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。

  • 执行导入

在 conf 目录下打开 git bash 窗口,执行以下命令:

1
shell复制代码sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t namespace-id(需要替换) -u nacos -w nacos

操作结束后,我们便可以在 nacos 控制台中看到配置列表,日后配置有需要修改便可以直接从这边修改,而不用修改目录文件:

  • 数据库配置

在 1.4.1 最新版中依然没有 sql 文件,所以我们还是需要另外下载:sql 下载地址

在 seata 数据中执行这个文件,生成三张表:

在我们的业务数据库中执行 undo_log 这张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码CREATE TABLE `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
  • 添加 log 文件

如果我们没有log输出文件,启动 seata 可能会报错,因此我们需要在 seata 目录下创建 logs 文件夹,在 logs 文件下创建 seata_gc.log 文件

  • 启动

做好了以上准备,我们便可以启动 seata 了,直接在 bin 目录下 cmd 执行 bat 脚本即可,启动结束便可在 nacos 中看到 seata 服务:

Seata 集成

在 Seata 安装的步骤中我们便完成了 Seata 服务端 的启动安装,接下来就是在项目中集成 Seata 客户端

  • 第一步:我们需要在 pom.xml 文件中添加两个依赖:seata 依赖 和 nacos 配置依赖
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
xml复制代码<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除依赖 指定版本和服务器端一致 -->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.1</version>
</dependency>

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>

注意: 这里需要排除 spring-cloud-starter-alibaba-seata 自带的 seata 依赖,然后引入我们自己需要的 seata 版本,如果版本不一致启动时可能会造成 no available server to connect 错误!

  • 第二步:我们需要把 restry.conf 文件复制到项目的 resource 目录下

  • 第三步:需要自己配置seata代理数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}

@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}

配置完数据源我们得在启动类的 SpringBootApplication 上排除Druid数据源依赖,否则可能会出现循环依赖的错误:

1
java复制代码@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  • 第四步:在 nacos 的配置文件控制台中加入我们服务的事务组项:
1
2
properties复制代码service.vgroupMapping + 服务名称 = default
group为: SEATA_GROUP

  • 第五步:项目中配置修改

  • 第六步:开启全局事务

这步就是最终一步了,在我们需要开启事务的方法上添加 @GlobalTransactional 注解,类似于我们单体事务添加的@Transactional

Seata 测试

我们现在回到项目中,在上面的示例演示中,我们已经知道了如果库存服务发生异常,会出现的情况是,库存没有减少,而订单依然会生成。那我们如果增加了 Seata 来管理全局事务,情况是否会有所改变?我们测试如下:

库存服务已经了异常:

看下数据库数据:

看样子我们全局事务已经生效了,事务也已经完美的控制住了!

而我们创建的 undo_log 这张表在管理事务中也启动了重要的作用:

看完了以上操作,我们趁热打铁来梳理一下其中的执行流程,让你印象更加深刻些~

相信看完这张图,你对 Seata 执行事务的流程也更加熟悉了吧!

这还没结束,我们接着来看看其中的一些要点:

  1. 每个 RM 都需要使用 DataSourceProxy 连接数据库,这样是为了使用 ConnectionProxy,使用数据源和数据连接代理的目的就是在第一阶段将 undo_log 和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有 undo_log 产生!
  2. 在第一阶段的 undo_log 中存放了数据修改前和修改后的值,为事务回滚做好准备,所以第一阶段就已经将分支事务提交,也就释放了锁资源!
  3. TM 开启全局事务后,便会将 XID 放入全局事务的上下文中,我们通过 feign 调用也会将 XID 传入下游服务中,每个分支事务都会将自己的 Branch ID 与 XID 相关联!
  4. 第二阶段如果全局事务是正常提交,那么TC 会通知各分支参与者提交分支事务,各参与者只需要删除对应的 undo_log 即可,并且可以异步执行!
  5. 第二阶段如果全局事务需要回滚,那么 TC 会通知各分支事务参与者回滚分支事务,通过 XID 和 Branch ID 找到相应的 undo_log 日志,通过回滚日志生成反向 SQL 并执行,完成事务提交之前的状态,如果回滚失败便会重试回滚操作!

END

到这里,一篇分布式事务就讲完了,我们回顾下,从分布式事务的五种解决方案到引出 Seata 的使用,小菜同学真是用心良苦~ 言归正传,看完后吸收了多少,动动小手,写写代码,让知识与你更亲近~

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

本文转载自: 掘金

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

刚火了的中台转头就拆,一大波公司放不下又拿不起来!

发表于 2021-03-29

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

离数学越远的代码,价值越低!

代码编程是对数学逻辑的具体实现,就相当于用砖头盖个厕所、码个猪圈、砌出个砖墙等是一样,砖还是那批5毛钱的砖,但盖在哪里盖出了啥价值就不一样了!

程序员也一样,你码的砖是公司里的;核心组件、通用模块、高并发业务还是一些ERP查询、接口包壳、屎山寻宝呢?通常那些复杂的业务逻辑或者具备一定技术深入的核心组件,才是最让人程序员快速成长的地方。

当然有些时候没有办法,不是不想做而是没得机会,或是因为初入职场、或是由于部门较差、也可能更多的是当前自身能力不足等等。但终究成长是自己事情,有了方向快是最大的障碍,脚踏实地把自己武装起来,才有谈判的机会!

二、为什么建中台?

1. 什么时候热的

通过百度指数搜索中台关键词,发现它是从19年5月21日突然热起来的,如下图;

百度指标搜索

  • 百度指标搜索:index.baidu.com/v2/main/ind…
  • 19年以前也并不是没有中台一词,只不过到了这一天像是被神化了一样,各个公司都要搞中台,被评论成上中台找死,不上中台等死!

2. 怎么就热了呢

说来奇怪怎么中台就热了呢,发生了啥?

腾讯汤道生:腾讯开放中台能力 助力产业升级

  • 啊!怪不得,是流量大佬腾讯于19年5月21日召开了全球数字生态大会,会议上腾讯高级副总裁汤道生提出“开放中台能力,助力产业升级”。
  • 原文:腾讯汤道生:腾讯开放中台能力 助力产业升级

3. 中台从哪来的

你玩过《海盗奇兵》吗?那《部落冲突》、《皇室战争》呢?咋滴,玩游戏还和中台有关系?

芬兰游戏公司Supercell 海盗奇兵

  • supercell(超级细胞),芬兰移动游戏巨头。拥有《部落冲突》、《卡通农场》、《海岛奇兵》、《皇室战争》和《荒野乱斗》 [1] 等全球热门游戏。
  • 芬兰移动游戏巨头supercell在2016年3月宣布,公司旗下游戏每日活跃用户(DAU)人数已经突破1亿。这家公司的CEO埃卡·潘纳宁(Ilkka Paananen)在推特上分享了这个消息,并向全球玩家表示感谢。
  • 在Supercell,每个独立游戏开发团队,称为“细胞”团队,核心人员通常只有5人,最多也不会超过7人。员工虽然少,但都是行业顶尖人才,还有充分的自由度。
  • 团队自己决定做什么样的产品,然后最快时间推出产品公测版,看看游戏是否受用户欢迎。如果用户不欢迎,迅速放弃这个产品,再进行新的尝试,期间几乎没有管理角色的介入。
  • 团队研发的产品失败后,不但不会受到惩罚,甚至还会举办庆祝仪式,以庆祝他们从失败中学到了东西。
  • 2015年年中,马云带领阿里巴巴集团高管,拜访了位于芬兰赫尔辛基的移动游戏公司Supercell。
  • 腾讯控股与其他参与财团已于2016年6月21日下午6点左右(北京时间)发布最新消息,确认已同意透过买方(财团的全资附属公司)收购Supercell的大部分股权。

综上,一个马老板收购了大部分股权,另一个马老板从 Supercell 团队开发模式,闻到中台的味道,细胞和部落 对应 小前台和大中台,至此半年后每一个程序员都被中台洗礼了。

三、建了哪些中台?

1. 技术中台

  • 难度:⭐⭐⭐⭐
  • 描述:技术中台提供了建设系统所需的基础设施、开发环境、数据服务、分布式能力等各类底层技术问题,同时技术中台有时也涵盖了研发中台的概念,主要是为了帮助工程的快速搭建、测试、集成、交付、运维、监控等。
  • 备注:技术中台基本是每个公司必备的,但可能每个公司会有多套测试环境、预发环境、上线环境,以及各类技术组件存在多套。建设中台的时候需要把这些能力进行整合,统一建设,统一维护。

2. 数据中台

  • 难度:⭐⭐⭐⭐
  • 描述:数据中台提供数据采集、运算、分析、算法等数据动作,并提供相应的数据服务;量化指标、人群标签、知识图谱、业务报表等。

3. 业务中台

  • 难度:⭐⭐⭐⭐⭐
  • 描述:业务中台提供可复用的服务能力,例如:交易、支付、活动、用户、订单等,这些服务可以标准化、简单化、统一化。
  • 备注:中台最想也最难的就是对业务中台的处理,支持浅了满足不了业务诉求、支持深了又太个性化满足不了所有需求。同时每一块业务拆分时可不只是系统,还有相应的业务、产品、运营,他们该如何提需求又提给谁。提的太复杂中台做不了,给后台做,做多了又想着平台化了。所以这也是最难的一块!

四、刚建好又要拆?

原来是建中台火,现在突然变成拆中台了。如果不是阿里自己说要拆中台,可能其他人也不敢说!

拆中台的起因是阿里内网说中台太厚了,影响到业务发展和敏捷响应能力。为啥这么说呢?

说白了,中台、低代码这些概念的指导结果,都是为了通用性服务的组装和编排。对于创新型颠覆式的需要快速试错的业务场景,就不太容易使用中台搭建。

但中台很适合类似盒马这样的场景诞生,有用户、有订单、有支付、有营销一整套的服务在中台都可以支撑,对于快速建设同类服务就变得非常容易。

可一些创新性,中台不具备或者不完全具备的服务,在通过前台、中台、后台,就变得非常困难,所有的需求没得把中台击穿就已经错过了市场。所以说中台太厚了,要拆中台。

1. 新需求响应难度增加

当中台为了通用性、共用性、平台性的原则建设新需求的时候,实际对业务响应的敏捷度就是下降的。

这包括一个新需求,不需要你的流程太长、也不需要你的通用性、甚至可能不需要你做完整的分库分表、数据采集、接口通用等等,如果你都按照中台的方式建设,那么这一个小需求的整体时间成本都将翻倍。

所以当这样的需求越来越多以后,你会发现建设的中台并没有沉淀下可复用的服务,这些服务最终后被前台系统沉淀下来。本来希望是中台做的厚一些,现在看是前台变得更厚了,前台对中台的依赖也越来越小了。这主要是因为前台离需求变化最近,敏锐度最高

2. 服务集成复杂度增加

中台提供了大量可复用的接口,但一个需求的实现会需要很多中台的接口集成,最终因为这些接口串联、组合、调试都过于冗长,使得效率不增反降。

原本一个需求由一个组可以实现,现在依赖中台需要很多组开会、协同、排期,严重拖慢了交付的进度,同时也不一定能提高交付质量。

3. 可复用实现难度增加

如果为了可复用则需要把一个需求放大,考虑它会发展成什么样,将来要扩展出哪些功能,留出什么样的口子,打哪种地基建设。基于各项的考虑把各类支撑需求的服务抽象化、去业务化,提取共性支撑业务组装。

这就像中间件的建设是为了屏蔽底层差异化一样,而你屏蔽的时候各类业务的差异化,而一个业务需求的变更都可能会影响到实际抽离出的业务组件该如何支撑。如果因为中台的通用性不能支持差异化需求,那么这类需求就会被建设在前台。

所以一个公司原本就没有很深、很广、很足的业务场景覆盖度,那么中台的建设会成为需求的绊脚石,投入的人力也将增大,每一次需要构建和完善时也会成为中台建设的灾难。

五、总结

  • 综上我们看到中台并不是没有益处,但也不是什么都能干。只是离业务太远就追不上业务的变化,离的太近有靠近前台,所以现在希望把中台做的薄一些,能快速的支撑业务发展和敏捷为导向。
  • 如果公司没有那么个需求和实力,就算想建中台也不要一下步子太大,最后可能中台建完了,公司受不了了。阿里拆中台拆也不是完全的拆,因为已经有中台可以很好支撑的场景了,那么需要快速变化的场景可以交给业务团队。
  • 无论是中台、低代码,相对于个人技术成长来说,都是看你在每一场技术游戏中,承担了什么角色、留下了什么价值,不会有永远稳定一成不变的技术组织,只需要关心在变化中不断积累个人成长所需的知识。

六、系列推荐

  • 方案设计:基于IDEA插件开发和字节码插桩技术,实现研发交付质量自动分析
  • 技术扫盲:关于低代码编程的可持续性交付设计和分析
  • 工作两三年了,整不明白架构图都画啥?
  • 不重复造轮子都是骗小孩的,教你手撸 SpringBoot 脚手架!
  • 《Java 面经手册》PDF,全书 417 页 11.5 万字,完稿&发版!

本文转载自: 掘金

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

使用 Golang 玩转 Docker API Go主题

发表于 2021-03-28

Docker 提供了一个与 Docker 守护进程交互的 API (称为Docker Engine API),我们可以使用官方提供的 Go 语言的 SDK 进行构建和扩展 Docker 应用程序和解决方案。

安装 SDK

通过下面的命令就可以安装 SDK 了:

1
bash复制代码go get github.com/docker/docker/client

管理本地的 Docker

该部分会介绍如何使用 Golang + Docker API 进行管理本地的 Docker。

运行容器

第一个例子将展示如何运行容器,相当于 docker run docker.io/library/alpine echo "hello world":

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
go复制代码package main

import (
"context"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

reader, err := cli.ImagePull(ctx, "docker.io/library/alpine", types.ImagePullOptions{})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, reader)

resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"echo", "hello world"},
}, nil, nil, "")
if err != nil {
panic(err)
}

if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}

statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
panic(err)
}
case <-statusCh:
}

out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
panic(err)
}

stdcopy.StdCopy(os.Stdout, os.Stderr, out)
}

后台运行容器

还可以在后台运行容器,相当于 docker run -d bfirsh/reticulate-splines:

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
go复制代码package main

import (
"context"
"fmt"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

imageName := "bfirsh/reticulate-splines"

out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
panic(err)
}
io.Copy(os.Stdout, out)

resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName,
}, nil, nil, "")
if err != nil {
panic(err)
}

if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}

fmt.Println(resp.ID)
}

查看容器列表

列出正在运行的容器,就像使用 docker ps 一样:

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
go复制代码package main

import (
"context"
"fmt"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
panic(err)
}

for _, container := range containers {
fmt.Println(container.ID)
}
}

如果是 docker ps -a,我们可以通过修改 types.ContainerListOptions 中的 All 属性达到这个目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// type ContainerListOptions struct {
// Quiet bool
// Size bool
// All bool
// Latest bool
// Since string
// Before string
// Limit int
// Filters filters.Args
// }

options := types.ContainerListOptions{
All: true,
}
containers, err := cli.ContainerList(ctx, options)
if err != nil {
panic(err)
}

停止所有运行中的容器

通过上面的例子,我们可以获取容器的列表,所以在这个案例中,我们可以去停止所有正在运行的容器。

注意:不要在生产服务器上运行下面的代码。

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
go复制代码package main

import (
"context"
"fmt"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
panic(err)
}

for _, container := range containers {
fmt.Print("Stopping container ", container.ID[:10], "... ")
if err := cli.ContainerStop(ctx, container.ID, nil); err != nil {
panic(err)
}
fmt.Println("Success")
}
}

获取指定容器的日志

通过指定容器的 ID,我们可以获取对应 ID 的容器的日志:

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
go复制代码package main

import (
"context"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

options := types.ContainerLogsOptions{ShowStdout: true}

out, err := cli.ContainerLogs(ctx, "f1064a8a4c82", options)
if err != nil {
panic(err)
}

io.Copy(os.Stdout, out)
}

查看镜像列表

获取本地所有的镜像,相当于 docker image ls 或 docker images:

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
go复制代码package main

import (
"context"
"fmt"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

images, err := cli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
panic(err)
}

for _, image := range images {
fmt.Println(image.ID)
}
}

拉取镜像

拉取指定镜像,相当于 docker pull alpine:

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
go复制代码package main

import (
"context"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

out, err := cli.ImagePull(ctx, "alpine", types.ImagePullOptions{})
if err != nil {
panic(err)
}

defer out.Close()

io.Copy(os.Stdout, out)
}

拉取私有镜像

除了公开的镜像,我们平时还会用到一些私有镜像,可以是 DockerHub 上私有镜像,也可以是自托管的镜像仓库,比如 harbor。这个时候,我们需要提供对应的凭证才可以拉取镜像。

值得注意的是:在使用 Docker API 的 Go SDK 时,凭证是以明文的方式进行传输的,所以如果是自建的镜像仓库,请务必使用 HTTPS!

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
go复制代码package main

import (
"context"
"encoding/base64"
"encoding/json"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

authConfig := types.AuthConfig{
Username: "username",
Password: "password",
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
panic(err)
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)

out, err := cli.ImagePull(ctx, "alpine", types.ImagePullOptions{RegistryAuth: authStr})
if err != nil {
panic(err)
}

defer out.Close()
io.Copy(os.Stdout, out)
}

保存容器成镜像

我们可以将一个已有的容器通过 commit 保存成一个镜像:

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
go复制代码package main

import (
"context"
"fmt"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)

func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}

createResp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "alpine",
Cmd: []string{"touch", "/helloworld"},
}, nil, nil, "")
if err != nil {
panic(err)
}

if err := cli.ContainerStart(ctx, createResp.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}

statusCh, errCh := cli.ContainerWait(ctx, createResp.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
panic(err)
}
case <-statusCh:
}

commitResp, err := cli.ContainerCommit(ctx, createResp.ID, types.ContainerCommitOptions{Reference: "helloworld"})
if err != nil {
panic(err)
}

fmt.Println(commitResp.ID)
}

管理远程的 Docker

当然,除了可以管理本地的 Docker, 我们同样也可以通过使用 Golang + Docker API 管理远程的 Docker。

远程连接

默认 Docker 是通过非网络的 Unix 套接字运行的,只能够进行本地通信(/var/run/docker.sock),是不能够直接远程连接 Docker 的。

我们需要编辑配置文件 /etc/docker/daemon.json,并修改以下内容(把 192.168.59.3 改成你自己的 IP 地址),然后重启 Docker:

1
2
3
4
5
6
7
8
9
bash复制代码# vi /etc/docker/daemon.json
{
"hosts": [
"tcp://192.168.59.3:2375",
"unix:///var/run/docker.sock"
]
}

systemctl restart docker

修改 client

创建 client 的时候需要指定远程 Docker 的地址,这样就可以像管理本地 Docker 一样管理远程的 Docker 了:

1
2
go复制代码cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(),
client.WithHost("tcp://192.168.59.3:2375"))

总结

现在已经有很多可以管理 Docker 的产品,它们便是这样进行实现的,比如:portainer。

原文链接:k8scat.com/posts/play-…

本文转载自: 掘金

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

Golang 操作 mysql 数据库(六)|Go主题月

发表于 2021-03-28

安装驱动

与 大多编程语言一样,Golang 并没有自带任何数据库操作驱动。所以我们首先得安装第三方函数库。

这时候我也曾经有过疑问 Golang 中不是有 database/sql包?后来我搞明白了。database/sql包 提供了保证SQL或类SQL数据库的泛用接口,使用时必须注入一个数据库驱动。

由于我这次操作的为 mysql 数据库,所以使用的第三方库为:

github.com/go-sql-driv…

使用命令 go get -u github.com/go-sql-driver/mysql 给 Golang 安装 MySQL 驱动

1
2
3
4
bash复制代码$ go get -u github.com/go-sql-driver/mysql
go: finding github.com/go-sql-driver/mysql v1.5.0
go: downloading github.com/go-sql-driver/mysql v1.5.0
go: extracting github.com/go-sql-driver/mysql v1.5.0

连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"fmt"
)

func main() {
// 数据源语法:"用户名:密码@[连接方式](主机名:端口号)/数据库名"
dsn := "root@tcp(127.0.0.1:3306)/dianping"
db, err := sql.Open("mysql", dsn) // open() 方法不会真正的与数据库建立连接,只是设置连接需要的参数
if err != nil {
panic(err)
}

// err:=db.Ping() //连接数据库
defer db.Close()
}

操作数据库

mysql 建立测试表

1
2
3
4
mysql复制代码create table test1(
id int primary key,
name varchar(10)
);

添加

1
2
3
4
go复制代码sql:="insert into test1 values (1,'卡卡')"
result,_:=db.Exec(sql) // 执行SQL
num,_:=result.RowsAffected(); // 获取受影响的行数
fmt.Println("受影响的行数:",num)

查询

1
2
3
4
5
scss复制代码rows,_:=db.Query("select * from test1")
for rows.Next(){ //循环显示所有的数据
rows.Scan(&id,&name)
fmt.Println(id,"--",name)
}

本文转载自: 掘金

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

1…696697698…956

开发者博客

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