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

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


  • 首页

  • 归档

  • 搜索

【java GIUI】人机对弈五子棋

发表于 2024-04-26

在学校的Java课程中,我们被分配了一项有趣的任务:开发一款能够实现人机对弈的五子棋游戏。为了更好地理解Java GUI的运用,并与大家分享学习心得,我将整个开发过程记录在这篇博客中。欢迎大家阅读并提供宝贵的意见和建议!”

python版五子棋(讲解的更详细)

)编辑

1.绘制棋盘

)编辑

1.定义myPanel类。

myPanel相当于画板。

myPanel要继承 JPanel类,并要覆盖父类的paint方法,在paint方法里面写负责绘画的代码

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
java复制代码
public class myPanel extends JPanel {
static final int start = 6;
@Override
public void paint(Graphics g)
{
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);

}
public void DrawBackground(Graphics g)
{ //绘制背景
g.setColor(new Color(211, 152, 91));
g.fillRect(0, 0, 620,620);

}
public void DrawBorder(Graphics g)
{
//绘制外边框
g.setColor(Color.BLACK);
g.fillRect(5,5,610,5);
g.fillRect(610,5,5,610);
g.fillRect(5,610,610,5);
g.fillRect(5,5,5,610);
}

public void DrawChessBoard(Graphics g)
{
g.setColor(Color.BLACK);
//画横线
for (int i = 0; i < 19; i++) {
g.drawLine(6+i*32,6,6+i*32,614);
}
//画竖线
for (int i = 0; i < 19; i++) {
g.drawLine(6,6+i*32,614,6+i*32);
}

}
//画棋子
public void DrawChess(Graphics g,int x,int y,int type){
switch (type){
case 1:
g.setColor(Color.BLACK);
break;
case 2:
g.setColor(Color.WHITE);
}
g.fillOval(x*32+start,y*32+start,30,30);

}


}

2.定义myFrame类。

myFrame相当于窗口,画板要放在窗口里。

myFram要继承 JFram类,在初始化函数设置窗口参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public class MyFrame extends JFrame {
myPanel mp = null;
public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}

static public void main(String[] args) {
MyFrame mf = new MyFrame();
}
}

2.核心功能

1.实现下棋功能

1.定义相关变量

1
2
3
4
5
6
7
8
java复制代码    //偏移量
static final int IndexOffset = -10;
static final int ChessOffset = -10;
//黑子下棋标记
static boolean black = true;
boolean gameIsOver = false;

int chess[][]=new int[19][19];

2.添加事件监听

myPanel实现 MouseListener接口

重写mouseClicked方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Override
public void mouseClicked(MouseEvent e) {
//计算棋子坐标
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋

int x = (e.getX() - IndexOffset) / 32;
int y = (e.getY() - 32 - IndexOffset) / 32;
System.out.println(y);
chess[x][y] = 1;
this.repaint();

}

myFrame添加事件监听

1
2
3
4
5
6
7
8
9
10
11
java复制代码    public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.addMouseListener(mp);
this.setVisible(true);
}

2.实现自动下棋

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
java复制代码
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
//# 3表示下棋的位置




private static final int[][] cdata = {

{3, 1, 1, 1, 1}, {1, 3, 1, 1, 1}, {1, 1, 3, 1, 1}, {1, 1, 1, 3, 1}, {1, 1, 1, 1, 3},

{2, 2, 3, 2, 2}, {2, 2, 2, 3, 2}, {2, 3, 2, 2, 2}, {3, 2, 2, 2, 2}, {2, 2, 2, 2, 3},

{3, 1, 1, 1, 0}, {1, 1, 1, 3, 0}, {1, 1, 3, 1, 0}, {1, 3, 1, 1, 0},

{3, 2, 2, 2, 0}, {2, 3, 2, 2, 0}, {2, 2, 3, 2, 0}, {2, 2, 2, 3, 0},

{1, 1, 3, 3, 0}, {3, 1, 1, 0, 0}, {0, 1, 3, 1, 0},

{3, 2, 2, 0, 0}, {2, 2, 3, 0, 0}, {1, 3, 1, 0, 0},

{3, 1, 0, 0, 0}, {1, 3, 0, 0, 0}

};

2.选出最佳下棋位置

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
java复制代码    public Point getBestPoint() {
//记录最低分(分值越低,优先级越高)
int score = 100;
//最佳位置
Point point = new Point(0, 0);
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {

for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int cnt = 0;
for (int[] e : cdata) {
int m;
int x = i;
int y = j;
int bestX = 0;
int bestY = 0;
for (m = 0; m < 5; m++) {
if (e[m] == 3 && chess[x][y] == 0) {
bestX = x;
bestY = y;
} else {
if (chess[x][y] != e[m]) {
break;
}
}
x += dx[k];
y += dy[k];
if (x < 0 || x >= 19 || y < 0 || y >= 19) {
break;
}

}
if (m == 5) {
if (cnt < score) {
score = cnt;
point.x = bestX;
point.y = bestY;
}
break;
}
cnt++;
}
}

}
}
if (score < 100) {
return point;
} else {
int x = (int) (Math.random() * 19);
int y = (int) (Math.random() * 19);
while (chess[x][y] != 0) {
x = (int) (Math.random() * 19);
y = (int) (Math.random() * 19);
}
return new Point(x, y);
}

}

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
29
30
31
32
33
34
35
36
37
38
java复制代码   public boolean check() {
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int x = i;
int y = j;
int m;
boolean flag = true;
for (m = 0; m < 4; m++) {
int tx = x + dx[k];
int ty = y + dy[k];
if (tx < 0 || tx > 19 || ty < 0 || ty > 19) {
flag = false;
break;
}
if (chess[x][y] != chess[x + dx[k]][y + dy[k]]) {
flag = false;
break;
} else if (chess[x][y] == 0) {
flag = false;
break;
}
x = tx;
y = ty;
}
if (flag) {
gameIsOver = true;
return true;
}

}
}
}
return false;
}

4.事件循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码   @Override
public void paint(Graphics g) {
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);
DrawChess(g);
//开始游戏

if (!gameIsOver)
game();

//游戏结束
if (check()) {
gameOver(g);
}

}

完整代码

myPanel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
java复制代码import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
*
*
* @author yuwei
* @date 23:29 2024/4/23
*/
public class myPanel extends JPanel implements MouseListener {
//偏移量
static final int IndexOffset = -10;
static final int ChessOffset = -10;
//黑子下棋标记
static boolean black = true;
boolean gameIsOver = false;

//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
//# 3表示下棋的位置


private static final int[][] cdata = {

{3, 1, 1, 1, 1}, {1, 3, 1, 1, 1}, {1, 1, 3, 1, 1}, {1, 1, 1, 3, 1}, {1, 1, 1, 1, 3},

{2, 2, 3, 2, 2}, {2, 2, 2, 3, 2}, {2, 3, 2, 2, 2}, {3, 2, 2, 2, 2}, {2, 2, 2, 2, 3},

{3, 1, 1, 1, 0}, {1, 1, 1, 3, 0}, {1, 1, 3, 1, 0}, {1, 3, 1, 1, 0},

{3, 2, 2, 2, 0}, {2, 3, 2, 2, 0}, {2, 2, 3, 2, 0}, {2, 2, 2, 3, 0},

{1, 1, 3, 3, 0}, {3, 1, 1, 0, 0}, {0, 1, 3, 1, 0},

{3, 2, 2, 0, 0}, {2, 2, 3, 0, 0}, {1, 3, 1, 0, 0},

{3, 1, 0, 0, 0}, {1, 3, 0, 0, 0}

};
int chess[][] = new int[20][20];

@Override
public void paint(Graphics g) {
//调用父类的paint初始化画笔
super.paint(g);
//绘制背景
DrawBackground(g);
//绘制外边框
DrawBorder(g);
//绘制棋盘
DrawChessBoard(g);
DrawChess(g);
//开始游戏

if (!gameIsOver)
game();

//游戏结束
if (check()) {
gameOver(g);
}

}

public void gameOver(Graphics g) {
Font font = new Font("Arial", Font.BOLD, 24);
g.setColor(Color.red);
g.setFont(font);
g.drawString("游戏结束", 270, 270);
}

public void DrawBackground(Graphics g) { //绘制背景
g.setColor(new Color(211, 152, 91));
g.fillRect(0, 0, 620, 620);

}

public void DrawBorder(Graphics g) {
//绘制外边框
g.setColor(Color.BLACK);
g.fillRect(5, 5, 610, 5);
g.fillRect(610, 5, 5, 610);
g.fillRect(5, 610, 610, 5);
g.fillRect(5, 5, 5, 610);
}

public void DrawChessBoard(Graphics g) {
g.setColor(Color.BLACK);
//画横线
for (int i = 0; i < 19; i++) {
g.drawLine(6 + i * 32, 6, 6 + i * 32, 614);
}
//画竖线
for (int i = 0; i < 19; i++) {
g.drawLine(6, 6 + i * 32, 614, 6 + i * 32);
}

}

//画棋子
public void DrawChess(Graphics g) {
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
if (chess[i][j] == 1) {
g.setColor(Color.BLACK);
} else if (chess[i][j] == 2) {
g.setColor(Color.WHITE);
} else {
continue;
}
g.fillOval(i * 32 + ChessOffset, j * 32 + ChessOffset, 30, 30);
}
}


}

public Point getBestPoint() {
//记录最低分(分值越低,优先级越高)
int score = 100;
//最佳位置
Point point = new Point(0, 0);
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {

for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int cnt = 0;
for (int[] e : cdata) {
int m;
int x = i;
int y = j;
int bestX = 0;
int bestY = 0;
for (m = 0; m < 5; m++) {
if (e[m] == 3 && chess[x][y] == 0) {
bestX = x;
bestY = y;
} else {
if (chess[x][y] != e[m]) {
break;
}
}
x += dx[k];
y += dy[k];
if (x < 0 || x >= 19 || y < 0 || y >= 19) {
break;
}

}
if (m == 5) {
if (cnt < score) {
score = cnt;
point.x = bestX;
point.y = bestY;
}
break;
}
cnt++;
}
}

}
}
if (score < 100) {
return point;
} else {
int x = (int) (Math.random() * 19);
int y = (int) (Math.random() * 19);
while (chess[x][y] != 0) {
x = (int) (Math.random() * 19);
y = (int) (Math.random() * 19);
}
return new Point(x, y);
}

}

public boolean check() {
//每次都要遍历四个方向 左下 下 右下 右
int[] dx = {-1, 0, 1, 1};
int[] dy = {1, 1, 1, 0};
for (int i = 0; i < 19; i++) {
for (int j = 0; j < 19; j++) {
for (int k = 0; k < 4; k++) {
int x = i;
int y = j;
int m;
boolean flag = true;
for (m = 0; m < 4; m++) {
int tx = x + dx[k];
int ty = y + dy[k];
if (tx < 0 || tx > 19 || ty < 0 || ty > 19) {
flag = false;
break;
}
if (chess[x][y] != chess[x + dx[k]][y + dy[k]]) {
flag = false;
break;
} else if (chess[x][y] == 0) {
flag = false;
break;
}
x = tx;
y = ty;
}
if (flag) {
gameIsOver = true;
return true;
}

}
}
}
return false;
}

public void game() {

if (check()) {
return;
}
if (black) {
Point point = getBestPoint();
chess[point.x][point.y] = 1;
black = false;
this.repaint();
}


}


@Override
public void mouseClicked(MouseEvent e) {
//计算棋子坐标
//# 0表示空棋
//# 1表示黑棋
//# 2表示白棋
if (!black&&!gameIsOver) {
int x = (e.getX() - IndexOffset) / 32;
int y = (e.getY() - 32 - IndexOffset) / 32;
chess[x][y] = 2;
black = true;
this.repaint();

}

}

@Override
public void mousePressed(MouseEvent e) {

}

@Override
public void mouseReleased(MouseEvent e) {

}

@Override
public void mouseEntered(MouseEvent e) {

}

@Override
public void mouseExited(MouseEvent e) {

}
}

myFrame

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复制代码import javax.swing.*;

/**
* 用户服务
*
* @author yuwei
* @date 23:42 2024/4/23
*/
public class MyFrame extends JFrame {
myPanel mp = null;
public MyFrame() {
mp = new myPanel();
this.setTitle("五子棋");
this.setSize(620, 620);
//添加画板
this.add(mp);
//点击窗口叉叉退出程序
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.addMouseListener(mp);
this.setVisible(true);
}

static public void main(String[] args) {
MyFrame mf = new MyFrame();
}
}

感谢阅读,希望本文对你有所帮助

本文转载自: 掘金

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

Nestjs用装饰器从0到1实现HTTP Get请求

发表于 2024-04-26

前言

本文是基于小满zs的nest教学视频的个人学习笔记,大家感兴趣可以去看看原文
xiaoman.blog.csdn.net/article/det…

我们通过axios库发送http请求,首先安装axios

1
sh复制代码npm i axios -S

然后定义一个控制器 Controller

1
2
3
4
5
6
7
8
ts复制代码class Controller {
constructor(){

}
getList(){

}
}

现在我希望Controller的getList()方法可以获取后端API返回的list,同时通过装饰器@GET去发送http请求,getList()方法中不包含任何与网络请求有关的代码。

那么我们要做的第一步,将后端API的URL作为参数传入装饰器函数中,再由装饰器函数发起axios请求,按照正常人想法,既然装饰器本质是一个函数,那我能不能直接将URL作为参数,传入装饰器函数中呢?

1
2
3
4
5
6
7
8
9
10
11
12
ts复制代码const GET:MethodDecorator = (target, key, scriptor, URL:string)=>{
cosnole.log(URL)
}

class Controller{
constructor(){}

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
getList() {

}
}

image.png
可以发现代码报错,MethodDecorator函数类型的参数是固定的,不能随便添加。
因为TS严格约束数据类型,因此通过|来添加参数的方法并不可取,至少这种投机取巧的方式用的多了,项目便很难维护。所以现在,是高阶函数出场的时候了。

既然我们要将URL作为参数传入装饰器函数,但是装饰器函数不能接收新的参数。我们不妨在外面再套一层函数,最外面的一层函数接收URL,然后返回装饰器函数。由于装饰器函数引用了外层函数的URL,形成了一个闭包!我们的问题完美解决了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts复制代码const GET = (URL: string):MethodDecorator=>{
return (target, key, scriptor)=>{
console.log(URL)
}
}

class Controller{
constructor(){}

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
getList() {

}
}

image.png

这种高阶函数就叫做装饰器工厂,装饰器的本质就是一个高阶函数。

接下来我们就可以继续编写装饰器函数中的逻辑:发送HTTP Get请求,然后将结果返回给装饰的getList()函数

这里我们复习一下方法装饰器函数的三个默认参数

  • 原型对象
  • 方法名称
  • 属性描述符
    • 可写:writable
    • 可枚举:enumerable
    • 可配置:configurable
    • ?value:对应的函数

所以我们可以通过descriptor属性描述符的value属性,获取到装饰的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ts复制代码import axios from "axios";

const GET = (url: string): MethodDecorator => {
return (target, key, descriptor) => {
//获取到装饰的函数
const fn = descriptor.value as Function;
//自定义参数status,用来传递状态码
let status = 0
//发送get请求
axios.get(url).then(res => {
//如果get请求发送成功,将返回的结果和其他自定义参数传递给getList函数
status = 200
fn(res, status)
}).catch(err => {
status = 500
fn(err, status)
})
}
}

然后我们使用@Get装饰getList方法,然后再getList函数里接收返回的参数。这句话是什么意思呢?我来解释给你听:
我们再axios发起Get请求后,调用了fn()函数,并往里面传递了一系列参数

1
2
3
4
5
6
7
8
ts复制代码        axios.get(url).then(res => {
//如果get请求发送成功,将返回的结果和其他自定义参数传递给getList函数
status = 200
fn(res, status)
}).catch(err => {
status = 500
fn(err, status)
})

而fn()函数是我们从方法装饰器的属性描述符中获取的

1
2
ts复制代码        //获取到装饰的函数
const fn = descriptor.value as Function;

结合起来就是,fn()函数接收到的参数,会传递给原方法,即被getList()函数接收

1
2
3
4
5
6
7
8
9
10
ts复制代码class Controller {
constructor() { }

@GET("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10")
//注意这里,我定义了两个参数,这两个参数就是从@GET装饰器传来的
getList(res: any, status: number) {
console.log(res.data)
console.log(status)
}
}

这样通过装饰器实现了,将发送HTTP Get请求的逻辑全部集中到@GET返回的装饰器函数,将Get请求返回的结果处理逻辑,全部集中到了getlist()方法中。

总结

我们通过定义装饰器工厂函数(高阶函数),解决了向装饰器传递自定义参数的问题
通过装饰器,成功将发送Get请求逻辑和处理Get请求逻辑分开

本文转载自: 掘金

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

Docker入门到精通《Nexus的搭建与使用》

发表于 2024-04-26

一、什么是私有镜像仓库?

镜像仓库(Docker Registry)有公有和私有的两种形式:

  • 公共仓库:例如Docker官方的Docker Hub,国内也有一些云服务商提供类似于Docker Hub的公开服务,比如网易云镜像服务、DaoCloud镜像服务、阿里云镜像服务等。
  • 除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。比如Nexus 、Harbor 、等。

二、私有仓库能做什么?

  • 可以存储公司私有镜像,避免暴露到外网。
  • 快速交付:当应用程序开发完成后,可以直接打包成一个镜像,将镜像上传到私有仓库后,可以在任何装有Doker的机器上下载该镜像,并运行为一个容器。

image.png

三、Nexus 、Harbor的区别

  • Harbor:可以自建文件夹进行分组这点就非常好。其实说实话,作为一个私有的镜像仓库,Harbor已经做得很好的了,唯一的缺点是它无法帮你下载镜像。因为在kubernetes环境下,你肯定有去公网拉镜像的需求,无论是官方还是非官方。你不可能因为这个而特地给你的所有node节点开通外网访问吧,这样风险太多且不可控。在我看来,整个kubernetes集群都不能访问外网。
  • Nexus :当你需要拉公网镜像的时候,你只要向它发起请求,它如果本地没有,就会自动去你配置的镜像仓库下载,下载完成之后先在本地缓存一份,然后发送给你。你甚至不用给Nexus开通外网,只需要在一台可以访问外网的机器上搭建一个代理服务就行,让Nexus通过代理去下载。

四、Nexus的搭建

4.1 Docker安装Nexus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码# Step 1:保证服务器对Nexus端口开放

# Step 2:创建/opt/docker/nexus文件夹。
# -p: 没有就创建。
mkdir -p /opt/docker/nexus

# Step 3:放给最高权限,方便使用
chmod 777 -R /opt/docker

# Step 4:运行 nexus3 容器
docker run -d --restart=always -p 8081:8081 --name nexus_container -v /opt/docker/nexus:/nexus-data sonatype/nexus3

# Step 5:日志查看
docker logs -f nexus_container

五、Nexus的使用

5.1 登录Nexus

1
2
3
4
5
6
7
yaml复制代码# Step 1:安装完成后可访问Nexus管理平台:http:ip:端口

# Step 2:登录Nexus,默认管理员用户名:admin 密码:admin123,如果提示密码不对,需要到容器里面查看管理员admin密码。
# cat /home/nexus/data/admin.password


# Step 3:第一次登陆之后,一般提示修改密码!修改密码之后,重新登录!

image.png

image.png

image.png

5.2 配置Nexus

Nexus默认创建了几个仓库,如下:
image.png

5.2.1 仓库名称

仓库名称 说明
maven-central Nexus 对 Maven 中央仓库的代理,默认从repo1.maven.org/maven2/拉取ja…
maven-releasse Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,私库发行版jar,初次安装请将Deployment policy设置为Allow redeploy要求 releasse 版本
maven-snapshots Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,要求 snapshots 版本
maven-public Nexus 默认创建,仓库分组,把上面三个仓库组合在一起对外提供服务,在本地maven基础配置settings.xml或项目pom.xml中使用

5.2.2 仓库类型

仓库类型 说明
proxy 代理仓库:它们被用来代理远程的公共仓库,如maven中央仓库。
hosted 本地仓库:通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。
group 仓库组:用来合并多个hosted/proxy仓库,当你的项目希望在多个repository使用资源时就不需要多次引用了,只需要引用一个group即可。

5.3 创建 Nexus仓库

除了自带的仓库,有时候我们需要单独创建自己的仓库,按照默认创建的仓库类型来创建我们自己的仓库。点击Create Repository
Snipaste_2023-10-10_19-02-20.png

选择如下三种类型的仓库。
Snipaste_2023-10-10_19-02-41.png

5.3.1 Hosted 仓库

输入仓库名,点击创建即可。
Snipaste_2023-10-10_19-03-43.png

5.3.2 Proxy仓库

输入仓库名以及被代理仓库的URL,这里我输入阿里云的仓库地址,默认为中央仓库。
Snipaste_2023-10-10_19-07-00.png

5.3.3 Group仓库

根据group仓库的解释,group仓库是其他仓库的集合,所以需要将其他创建的仓库添加到组里。
Snipaste_2023-10-10_19-07-45.png

上面的仓库创建好之后就可以在首页看到了。
Snipaste_2023-10-10_19-08-17.png

5.4 通过 Nexus 下载 jar 包

5.4.1 创建一个新仓库

Nexus私服搭建好之后就可以通过Nexus下载jar包了。为了方便演示我在本地创建了一个空的仓库:new-repo
Snipaste_2023-10-10_19-10-43.png

5.4.2 修改Maven的配置,将新仓库作为默认仓库

Snipaste_2023-10-10_19-11-26.png
Snipaste_2023-10-10_19-11-45.png

5.4.3 修改镜像配置(这里我们之前都是配置的阿里云仓库,现在改为我们自己的Nexus仓库)

Snipaste_2023-10-10_19-16-53.png

1
2
3
4
5
6
xml复制代码     <mirror>
<id>maven-public</id>
<mirrorOf>central</mirrorOf>
<name>Maven public</name>
<url>http://192.168.11.164:8090/repository/maven-public/</url>
</mirror>

这里的 url 标签是这么来的:
Snipaste_2023-10-10_19-14-26.png
把上图中看到的地址复制出来即可。如果我们在前面允许了匿名访问,到这里就够了。但如果我们禁用了匿名访问,那么接下来我们还要继续配置settings.xml:
Snipaste_2023-10-10_19-17-08.png

1
2
3
4
5
xml复制代码  <server>
<id>maven-public</id>
<username>admin</username>
<password>admin</password>
</server>

注意:server 标签内的 id 标签值必须和 mirror 标签中的 id 值一样,用户名和密码是修改之后的。

5.4.4 验证

我们新建一个Maven项目然后引入一个依赖(这里我用fastjson2举例)来验证jar包是否是通过Nexus下载的。新建项目之后修改项目的Maven配置以及引入依赖。

Snipaste_2023-10-10_19-19-18.png

Snipaste_2023-10-10_19-20-32.png

Snipaste_2023-10-10_19-22-08.png

等待下载完成我们刷新对应的仓库可以发现jar包已经下载到Nexus里面了。
Snipaste_2023-10-10_19-28-29.png

5.5 将 jar 包部署到 Nexus

演示完通过Nexus下载jar包,接下该演示怎么将本地模块打包发布到Nexus私服,让其他的项目来引用,以更简洁高效的方式来实现复用和管理。

因为发布jar包涉及到snapshots和releases仓库,所以需要配置这两个的仓库的访问权限,同样的这是针对禁用了匿名访问的操作的,如果没有禁用匿名访问,这里依然不用配置。

5.5.1 需要配置的server:

Snipaste_2023-10-10_19-29-47.png
Snipaste_2023-10-10_19-33-08.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<server>
<id>maven-public</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>

5.5.2 配置pom.xml

然后在我们需要上传的 maven 项目中的pom.xml添加如下配置:
Snipaste_2023-10-10_19-33-58.png

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!-- 这里的 id 要和上面的 server 的 id 保持一致,name 随意写-->
<distributionManagement>
<repository>
<id>maven-releases</id>
<name>Releases Repository</name>
<url>http://192.168.11.164:8090/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>maven-snapshots</id>
<name>Snapshot Repository</name>
<url>http://192.168.11.164:8090/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>

5.5.3 部署到Nexus

然后点击部署deploy就可以将项目发布到Nexus了。
Snipaste_2023-10-11_10-28-23.png

然后刷新maven-snapshots仓库就可以发现,项目已经发布了。
Snipaste_2023-10-11_10-29-17.png

5.6 疑问

注意:为什么是发布到snapshot仓库呢?那如果想要发布到releases仓库该怎么做呢?

这是因为我们创建Maven的时候,版本号默认为1.0-SNAPSHOT,所以就对应发布到snapshot仓库了,只需要将版本号改为正式版本号就可以了。
Snipaste_2023-10-11_10-30-02.png

然后点击deploy就可以在releases仓库找到了。
Snipaste_2023-10-11_10-30-31.png

最后,介绍一下Maven几个常用命令的作用。

package 命令完成了项目编译、单元测试、打包功能。

install 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库。

deploy 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库。

本文转载自: 掘金

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

AI菜鸟向前飞 — LangChain系列之七 - 深入浅出

发表于 2024-04-26

开篇

EL简单实现,原理初探

AI菜鸟向前飞 — LangChain系列之六 - 深入浅出LCEL与Chain(上篇)

上一篇文档给大家介绍的Expression Language 特别提到了,


每个运行对象(即:Runnable)通过"|"(管道)连接组成了Chain,可以通过更快速书写且更易读的方式带来了很好的体验效果,在文章中的最后,我给大家展示了每一个Runnable对象的输入/输出Schema,可能有些小伙伴还是有点懵,这样我通过一个简单的实例,自己来实现一个`Pipeline`

程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码class MyPipeline:
def __init__(self, func):
self.func = func
def __or__(self, otherfunc):
def _func(*args, **kwargs):
return otherfunc(self.func(*args, **kwargs))
return MyPipeline(_func)

def __call__(self, *args, **kwargs) :
return self.func(*args, **kwargs)

def invoke(self, *args, **kwargs):
return self.__call__(*args, **kwargs)
再准备一下
1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def hello(name) -> str:
return f"hello,{name}。"

def welcome(greeting) -> str:
return f"欢迎来到我的公众号, {greeting}"

def attach_date(mstr) -> str:
return f"{mstr} 现在是{datetime.now().strftime("%Y-%m-%d")}。"

# 为什么这里要写这么麻烦,后面介绍RunnableLambda你就懂了
hello = MyPipeline(hello)
welcome = MyPipeline(welcome)
attach_date = MyPipeline(attach_date)

试出效果

1
2
3
scss复制代码# my_chain = mychain = hello.__or__(welcome).__or__(attach_date)
my_chain = hello | welcome | attach_date
print(test.invoke("Song榆钱儿"))

输出结果

  • 1
1
复制代码欢迎来到我的公众号, hello,Song榆钱儿。现在是2024-04-26。

分析过程

以图代言 —— 可以直接看懂通过管道(“|”)让可运行对象(Runnable)之间如何传递数据的。(这样也更好理解上一篇最后的内容)

图片

简单解析

1
2
arduino复制代码这里主要用到了Python的魔法函数__or__,所以"|"这里对于Python实现还是比较简单的,若大家对此感兴趣,可以持续关注我哈
同时,我们也可以看到LangChain底层也是通过这种“方式”实现的

图片

1
复制代码

图片

思考题

1
2
ini复制代码# LangChain还支持这种形式pipe连接多个Runnable对象,你可以自行实现下:)
my_chain = hello.pipe(welcome).pipe(attach_date)

LangChain的其它相关函数

Runnable对象支持的函数比较多,如下所示:官网API地址: 


[api.python.langchain.com/en/stable/r…](https://api.python.langchain.com/en/stable/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable)

这里挑比较有代表性的三个给大家介绍,

RunnableLambda

让我们再回到LangChain中,看看RunnableLambda是怎么用的

图片

RunnableLambda能将普通函数转换为Runnable对象,并使用EL(Expression Language)语法

with_fallbacks

程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码from langchain_openai import ChatOpenAI
from langchain_community.chat_models import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("{text}")
# 我的OpenAI Key已经过期,调用它肯定报错
model_openai = ChatOpenAI()
# 使用我本地启动的Ollama llama3大语言模型,肯定没问题
model = ChatOllama(model="llama3", temperature=0)

output_parser = StrOutputParser()
chain = prompt | model_openai.with_fallbacks([model]) | output_parser
response = chain.invoke({"text": "你好啊, AI小助手"})

print(response)

输出结果

图片

分析过程

图片

Bind

以后Tool相关知识的会用到它


程序&输出结果

图片

不加bind(stop...)呈现的效果如下:

图片

期待下篇吧~ ㊗️大家周末愉快

本文转载自: 掘金

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

采集 Kubernetes 容器日志最佳实践

发表于 2024-04-26

前言

指标、日志、链路是可观测的三大支柱,日志主要用于记录代码执行的痕迹,方便定位和排查问题。当前主流的应用都是以容器的方式运行在 Kubernetes 集群,由于容器的动态性,容器可能会频繁地创建和销毁。日志的采集和持久化变得尤为重要,以确保在容器生命周期结束后,仍然能够访问到运行时的信息。以下内容介绍如何利用观测云采集 Kubernetes 容器日志,并对采集的日志进行解析、查询、可视化分析和备份的整个流程。

接入方案

部署 DataKit 采集器

采集 Kubernetes 容器日志需要先部署 DataKit。

登录观测云控制台,点击「集成」 -「DataKit」 - 「Kubernetes」,下载 datakit.yaml ,拷贝第 3 步中的 token 。

编辑 datakit.yaml ,把 token 粘贴到 ENV_DATAWAY 环境变量值中“token=”后面,设置环境变量 ENV_CLUSTER_NAME_K8S 的值并增加环境变量 ENV_NAMESPACE,这两个环境变量的值一般和集群名称对应,一个工作空间集群名称要唯一。

1
2
yaml复制代码        - name: ENV_NAMESPACE
value: k8s-prod

把 datakit.yaml 上传到可以连接到 Kubernetes 集群的主机上,执行如下命令。

1
2
arduino复制代码kubectl apply -f datakit.yaml
kubectl get pod -n datakit

当看到状态是 “Running”后表示部署 DataKit 成功。

控制台日志采集

DataKit 默认采集了所有容器输出到控制台的日志(stdout/stderr),这些日志的特点是通过 kubectl logs 可以查看到。登录观测云控制台,点击「日志」 -「查看器」 ,可以看到已经采集到的日志,其中数据源默认展示的是容器的名称,接下来的采集中,会使用自定义数据来源。

DataKit 也提供了自监控功能,实时查看采集情况。DataKit 默认部署在 datakit namespace 下面,执行 kubectl exec 命令进入 DataKit 容器。

1
bash复制代码kubectl exec -it datakit-6rjjp -n datakit bash

再执行 datakit monitor,右下方的 logging/ 开头的行即是采集容器日志的实时监控数据。

默认的采集方式不是太灵活,这里推荐一种最佳的采集方式,把默认采集所有输出到控制台的日志关掉,通过染色的方式,在需要采集日志的 Deployment 部署文件中增加 annotation 方式指定是否需要采集、更改数据源名称以及为日志打 tags。

在 datakit.yaml 中增加下面的环境变量,即不采集任何控制台日志。

1
2
yaml复制代码        - name: ENV_INPUT_CONTAINER_CONTAINER_EXCLUDE_LOG
value: image:*

然后在应用的 Deployment yaml 文件中添加 annotation。

1
2
3
4
5
6
7
8
9
10
11
bash复制代码      annotations:
datakit/logs: |
[
{
"disable" : false,
"source": "log_stdout_demo",
"tags": {
"region": "hangzhou"
}
}
]

字段说明:

  • disable 是否禁用该容器的日志采集,默认是 false。
  • source 日志来源,非必填项。
  • tags key/value 键值对,添加额外的 tags,非必填项。

登录观测云控制台,点击「日志」 -「查看器」 ,可以看到已经采集到的日志。

容器内日志文件采集

对于容器内日志文件的采集,也是通过添加 annotations 的方式来实现采集的。

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码      annotations:
datakit/logs: |
[
{
"disable": false,
"type": "file",
"path":"/data/app/logs/log.log",
"source": "log_file_demo",
"tags": {
"region": "beijing"
}
}
]

字段说明:

  • disable 是否禁用该容器的日志采集,默认是 false。
  • type 默认为空是采集 stdout/stderr,采集文件必须写 file。
  • path 配置文件路径。如果是采集容器内文件,必须填写 volume 的 path,注意不是容器内的文件路径,是容器外能访问到的路径。
  • source 日志来源,非必填项。
  • tags key/value 键值对,添加额外的 tags,非必填项。

注意:需要把日志路径目录挂载到 emptyDir,这里挂的是 /data/app/logs。

1
2
3
4
5
6
7
yaml复制代码        volumeMounts:
- mountPath: /data/app/logs
name: varlog
......
volumes:
- name: varlog
emptyDir: {}

日志路径支持 glob 规则 进行批量指定,比如日志文件是 /tmp/opt/**/*.log ,挂载的目录必须高于通配的目录,比如挂载 /tmp 或 /tmp/opt。

登录观测云控制台,点击「日志」 -「查看器」 ,可以看到已经采集到的日志,当然也可以使用自定义的 tags 进行检索。

日志解析

为了通过日志中特定内容进行快捷筛选、关联分析,就需要使用 Pipeline 对日志进行结构化处理,比如提取trace_id、日志状态等。

下面是一条业务日志和对应的 Pipeline。

1
ini复制代码2024-04-11 11:10:17.921 [http-nio-9201-exec-9] INFO  c.r.s.c.SysRoleController - [list,48] - ry-system-dd 2350624413051873476 1032190468283316 - 查询角色列表开始
1
2
yaml复制代码grok(_, "%{TIMESTAMP_ISO8601:time} %{NOTSPACE:thread_name} %{LOGLEVEL:status}%{SPACE}%{NOTSPACE:class_name} - \[%{NOTSPACE:method_name},%{NUMBER:line}\] - %{DATA:service} %{DATA:trace_id} %{DATA:span_id} - %{GREEDYDATA:msg}")
default_time(time, "Asia/Shanghai")

成功解析出 trace_id、span_id、service 等标签,方便后续的快捷筛选、关联分析。

日志查询

观测云支持通过多种操作对日志数据进行查询和分析。

文本搜索

日志查看器支持关键词查询、通配符查询,* 表示匹配 0 或多个任意字符,? 表示匹配 1 个任意字符;若要将多个术语组合到一个复杂查询中,可以使用布尔运算符(AND/OR/NOT)连接。

术语可以是单词或者短语。比如:

  • 单个单词:guance;
  • 多个单词:guance test;(等同于 guance AND test)
  • 短语:”guance test”; (使用双引号可以将一组单词转换为短语)

搜索查询示例:

JSON 搜索

查看器原生支持对 JSON 格式 message 内容进行精确检索,搜索格式为: @key:value ,若为多层级 JSON 可用 “.” 承接,即 @key1.key2:value ,如图所示:

日志可视化分析

场景图表

观测云内置多种数据监控视图模版,用户可导入模板创建仪表板和查看器,并进行自定义编辑配置;或选择自定义创建方式,通过一系列设置构建数据洞察场景。比如,根据前面解析出来的 status 字段,统计一下 info、error 状态的日志分别有多少,可以通过以下步骤来创建可视化仪表板。

第一步:在场景->新建空白仪表板中,选择自己想要的视图类型。

第二步:选择日志数据源,设置过滤条件和分组,点击创建。

强大的关联能力

1、视图配置跳转链接

观测云提供链接功能,可以平滑跳转仪表板 & 查看器,实现数据联动分析、系统全面可观测。

  • 在视图设置页面,配置链接地址。

  • 再点击视图中的数据,即可跳转到对应的日志查看器,快速实现视图与查看器联动分析。

2、绑定内置视图

观测云还支持将视图保存为内置视图,并绑定到查看器中,方便在查看日志数据的同时,分析其他维度的数据。

查看日志详情时,即可查看上面绑定的内置视图,也可以绑定其他维度的视图,比如主机的指标视图等等。

日志告警

观测云提供开箱即用的监控模板来新建监控器;也支持自定义新建监控器,通过阈值检测、日志检测、突变检测、区间检测等十余种检测规则来设置检测规则和触发条件。开启监控器后,即可接收到由检测规则触发的相关异常事件告警。

其中,日志检测用于监控工作空间内基于日志采集器产生的的全部日志数据。支持基于日志的关键字设置告警,及时发现不符合预估行为的异常模式(如:日志文本数据中存在异常的标签),多适用于 IT 监控场景下的代码异常或任务调度检测等。

第一步:在监控->新建日志检测监控器。

第二步:设置检测规则和触发条件。

这里以日志内容包含”WARN”为例,设置超过100条时就触发告警。

第三步:编辑事件通知内容和告警策略,点击创建即可。

日志备份

观测云提供日志数据转发到观测云的对象存储及转发到外部存储的功能(包含观测云备份日志、AWS S3、华为云 OBS、阿里云 OSS 和 Kafka 消息队列)。用户可以自由选择存储对象,灵活管理日志备份数据。

日志备份

第一步:点击日志->数据转发

第二步:点击转发规则->新建规则

第三步:设置需要备份的数据源,和相关筛选条件,点击确定即可。

注意:该规则下的日志数据最低存储默认为 180 天,可以前往管理 > 设置 > 变更数据存储策略中修改数据转发存储策略。

查看备份数据

第一步:点击日志->数据转发,在下拉框选定规则。

第二步:自定义时间范围查询,可选择多个日期及定义开始时间和结束时间,时间会精确到小时,即可查询到备份数据。

更多日志备份相关操作,也可以阅读官方文档的详细介绍。

总结

通过以上方式,可以快速将部署在 Kubernetes 集群的各个业务系统的日志采集到观测云平台,实现日志采集、日志解析、查询分析、监控告警、归档备份等一整套解决方案。

本文转载自: 掘金

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

云原生✖️ AI 时代的微服务架构最佳实践—— CloudW

发表于 2024-04-26

活动介绍

CloudWeGo 开源两年多以来,社区发展迅速,生态日益丰富,落地企业用户已超过 40 家,涵盖 AI、电商、金融、游戏 、互联网等多个行业。同时,随着云原生技术和 AI 技术的持续蓬勃发展,我们发现企业用户也面临着越来越多性能、成本和稳定性方面的挑战,系统需要支持弹性伸缩和潮汐流量下的稳定性,因而也越发需要一套高性能、易扩展、功能丰富的微服务架构。

诚挚邀请企业用户和开发者共同参与 CloudWeGo 技术沙龙。活动将于2024年5月25日(周六)在上海举办,邀请广大技术同仁共同探讨在 云原生 xAI 浪潮之下,企业如何构建云原生微服务架构,来支持产品的快速迭代与发展。

  • 时间:2024年5月25日(周六)14:00-17:00
  • 地点:上海 · 漕河泾中心D栋F2

议题简介

本次活动分享议题将聚焦 CloudWeGo 相关技术功能实现,以及如何借力 CloudWeGo 开源项目帮助企业构建微服务等议题,将携手 CSDN 、infoQ、稀土掘金、火山引擎开发者社区、字节跳动技术团队作为合作伙伴同步进行宣传和直播。多位 CloudWeGo 社区 Maintainer 和 Committer 将分享包括微服务框架的对比和落地实践,以及基于 cwgo 代码生成工具的工程化实践等主题。另外我们还邀请了多位 CloudWeGo 的用户代表进行分享他们基于 CloudWeGo 的落地实践经验等精彩话题。最后我们也会围绕微服务相关热点话题进行圆桌讨论,和现场观众进行互动。

image.png

主题演讲:微服务框架对比、测试与迁移

  • 讲师:周启恒,CloudWeGo-Kitex Maintainer;李纪昀,CloudWeGo-Web&Doc Reviewer,CloudWeGo-Hertz Committer
  • 大纲: CloudWeGo 提供了高性能、高可靠的 Go 语言 RPC 框架 Kitex 以及 HTTP 框架 Hertz,助力用户高效搭建完备的企业级微服务架构。本次分享中,我们将从功能和性能多方面对比 CloudWeGo 微服务框架与开源框架 ,展示 Kitex 与 gRPC,Hertz 与 Gin 的差异与优势。此外,我们将分享关于框架迁移的操作实践和迁移的真实收益。

主题演讲:基于 Hertz 的微服务落地实践

  • 讲师:初泽良,字节跳动西瓜视频研发工程师
  • 大纲: Hertz 是一个 Golang 微服务 HTTP 框架,具有高易用性、高性能、高扩展性等特点。在本次演讲中,我们将介绍西瓜视频基于 Hertz 的微服务落地实践。我们将介绍西瓜视频微服务架构设计、Hertz 框架介绍、西瓜视频迁移 Hertz 过程及踩坑经验、落地 Hertz 后的收益。

主题演讲:从0到1基于 Kitex + Istio 的微服务系统建设

  • 讲师:Jason,Construct 服务端总监
  • 大纲: 在本次演讲中,我们将展示如何使用 Kitex 和 Istio 从0到1构建微服务架构,探讨技术栈和架构选择的理由及其实施细节。内容将包括系统兼容性策略、自动化流程、泳道以及通过监控和分布式追踪技术确保微服务可观测性和稳定性。

主题演讲:基于 cwgo 代码生成工具的工程化实践

  • 讲师:王鑫, CloudWeGo-Hertz Committer;鹿瑞超, CloudWeGo-Hertz Reviewer
  • 大纲: cwgo 是 CloudWeGo All in one 代码生成工具,整合了各个组件(hz,kitex)的优势,以提高开发者的体验。在本次演讲中,我们将从代码生成能力和工程化实践两方面介绍cwgo,了解如何通过使用 cwgo 简化代码生成过程和提高开发效率,实现工程化开发体验的提升。

圆桌讨论

  • 主持人:罗广明
  • 圆桌嘉宾:初泽良、Jason、周启恒
  • 大纲:
+ 微服务框架和中间件技术选型关注哪些方面?
+ 浅谈开源框架的易用性和其带来的研发效率在业务团队的价值
+ 浅谈 AI 对微服务框架演进和业务研发带来的影响
+ 现场 Q&A

立刻报名

访问活动页面即可报名注册,参与现场互动还有机会获得社区精美周边礼品。了解更多 CloudWeGo 项目相关信息请访问 www.cloudwego.cn 或 github.com/cloudwego 期待您的参与!


重磅,由字节跳动服务框架团队联合 CloudWeGo 开源社区出品的 《CloudWeGo 技术白皮书: 字节跳动云原生微服务架构原理与开源实践》 现已正式对外发布!本书总结了字节跳动自 2018 年以来的微服务架构演进之路,讲述了字节微服务架构的难点、编程语言的选择和开发框架的演进,以及流量激增后的流量治理模式和服务网格全面落地。白皮书中还详细介绍了电商、AI、金融、游戏相关行业的落地案例,同时探讨了在降本增效压力下微服务的性能提升和成本优化解决方案。下载地址:

  • www.cloudwego.cn/zh/
  • www.cloudwego.io/zh/

本文转载自: 掘金

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

Redis存储方式大揭秘,让你的数据飞上天! Redis存储

发表于 2024-04-26

Redis存储方式大揭秘,让你的数据飞上天!

哟,听说你最近的项目数据存储遇到了困难?那些老掉牙的关系型数据库已经跟不上你的脚步了?别怕,Redis来了!这位数据存储界的”网红”,绝对能让你的数据嗨起来!

Redis这家伙啊,就是一个高性能的key-value内存数据库。它凭什么这么牛?就凭它那恐怖的读写速度和花里胡哨的数据类型!今天,咱们就一起扒一扒Redis存储方式的底裤,看看它是怎么让你的数据又快又好存的!

内存存储,就是这么任性!

Redis的第一大特点,那就是内存存储!没错,你的数据在Redis里,就是直接住在内存里的!这意味着什么?这意味着Redis读写数据快得一批!

想想看,硬盘IO速度才100MB/s左右,而内存IO速度可以达到几十GB/s!这差距,不是一般的大啊!所以,当Redis处理海量数据的读写请求时,那速度,嗖嗖的,眨眼间就完事了!

举个例子,假设你的应用需要频繁地读取用户的基本信息。如果你把这些信息存在MySQL里,每次读取都得走一遍硬盘IO,那性能肯定上不去。但如果你把这些信息缓存在Redis里,读取速度那是杠杠的,啥时延都别想有!

当然,内存存储也不是完美无缺的。内存容量不够大,价格又贵。但Redis早就想到了这茬,它提供了一些机制来解决这些问题,比如数据淘汰策略和数据持久化,咱们一会儿再说。

数据类型,花样百出!

Redis的另一大亮点,就是它那花里胡哨的数据类型支持。别的key-value存储,通常就只支持简单的字符串类型。但Redis可不是吃素的,它的数据类型,那是相当丰富:

  • 字符串(String):这是最基础的类型,就像Memcached一样,一个key对一个value。
  • 哈希(Hash):这个类型有点儿像Java里的Map,一个key对应一个Map。
  • 列表(List):这个类型就像一个双向链表,你可以在列表的头部或尾部添加元素。
  • 集合(Set):这个类型跟Java中的HashSet差不多,它里面的键值对是无序且唯一的。
  • 有序集合(Sorted Set):这个类型跟Set类似,但里面的元素是有序的,每个元素都有一个分数(score)来排序。

有了这一箩筐数据类型,Redis就成了一个”多面手”,啥业务场景都能应对。

比如,你可以用String来缓存一些常用的对象,提高系统的响应速度:

1
2
3
4
5
6
7
8
// 将用户对象缓存到Redis
User user = new User(1, "Tom", 25);
String key = "user::" + user.getId();
redisTemplate.opsForValue().set(key, JSON.toJSONString(user));

// 从Redis获取用户对象
String userJson = redisTemplate.opsForValue().get(key);
User cachedUser = JSON.parseObject(userJson, User.class);

又比如,你可以用Hash来存储一个用户的详细信息:

1
2
3
4
5
6
7
8
9
10
// 存储用户信息到Hash
String userKey = "user::" + userId;
redisTemplate.opsForHash().put(userKey, "name", "Tom");
redisTemplate.opsForHash().put(userKey, "age", 25);
redisTemplate.opsForHash().put(userKey, "city", "New York");

// 获取用户信息从Hash
String name = (String) redisTemplate.opsForHash().get(userKey, "name");
int age = (int) redisTemplate.opsForHash().get(userKey, "age");
String city = (String) redisTemplate.opsForHash().get(userKey, "city");

再比如,你可以用List来实现一个简单的队列:

1
2
3
4
5
6
// 添加元素到队列
redisTemplate.opsForList().leftPush("queue", "task1");
redisTemplate.opsForList().leftPush("queue", "task2");

// 从队列获取元素
String task = redisTemplate.opsForList().rightPop("queue");

或者,你可以用Set来实现一个标签系统:

1
2
3
4
5
6
// 添加标签
redisTemplate.opsForSet().add("tags::user::1", "music", "movie", "travel");
redisTemplate.opsForSet().add("tags::user::2", "music", "sports");

// 获取共同的标签
Set<String> commonTags = redisTemplate.opsForSet().intersect("tags::user::1", "tags::user::2");

还有啊,你可以用Sorted Set来实现一个排行榜功能:

1
2
3
4
5
6
7
// 添加分数
redisTemplate.opsForZSet().add("leaderboard", "player1", 100);
redisTemplate.opsForZSet().add("leaderboard", "player2", 90);
redisTemplate.opsForZSet().add("leaderboard", "player3", 110);

// 获取排名
long rank = redisTemplate.opsForZSet().reverseRank("leaderboard", "player2");

看看,Redis的应用场景,是不是相当丰富?

数据淘汰,优胜劣汰!

咱们刚说了,内存存储的一个问题是容量有限。那Redis咋解决这个问题呢?答案就是:数据淘汰策略!

当Redis的内存使用到了某个程度,它就会自动触发数据淘汰机制。Redis有一堆数据淘汰策略让你挑:

  • volatile-lru:在设置了过期时间的数据中,挑最近最少使用的数据淘汰。
  • volatile-ttl:在设置了过期时间的数据中,挑快要过期的数据淘汰。
  • volatile-random:在设置了过期时间的数据中,随便挑数据淘汰。
  • allkeys-lru:在所有数据中,挑最近最少使用的数据淘汰。
  • allkeys-random:在所有数据中,随便挑数据淘汰。
  • noeviction:不淘汰数据,内存不够时,新写入操作会报错。

你可以根据自己的业务特点,选一个合适的淘汰策略。比如,如果你的数据都能再次获取,那选allkeys-lru就不错;如果你有一些”热点数据”,那就用volatile-lru,优先保证这些数据在内存里常驻。

举个例子,如果你用Redis缓存用户信息,你可能希望经常访问的活跃用户的信息能够常驻内存,不那么活跃的用户的信息就不那么care了。这时候,你就可以这样设置:

1
2
3
4
5
6
# 设置淘汰策略为volatile-lru
redis-cli config set maxmemory-policy volatile-lru

# 设置key的过期时间
redis-cli expire user::1 3600
redis-cli expire user::2 7200

这样,Redis就会在内存不够时,优先淘汰那些不怎么访问的用户信息,保留活跃用户的信息在内存中。是不是很智能?

数据持久化,安全第一!

除了数据淘汰策略,Redis还提供了数据持久化功能,保证数据安全。Redis的持久化有两种方式:RDB和AOF。

RDB(Redis Database)就是每隔一段时间,把内存中的数据快照写到磁盘上。这玩意儿默认就开启了,你可以设置自动备份的时间和文件名。RDB的好处是备份速度快,适合定期备份;坏处是两次备份之间如果出问题,可能会丢点数据。

你可以在redis.conf里设置RDB:

1
2
3
4
5
6
7
8
# 900秒内如果至少有1个key被修改,则执行bgsave (background save)
save 900 1  

# 300秒内如果至少有10个key被修改,则执行bgsave  
save 300 10

# 60秒内如果至少有10000个key被修改,则执行bgsave
save 60 10000

AOF(Append Only File)则是把Redis的操作日志追加写到文件里。每次有写操作,AOF就会同步地把操作写到硬盘上的AOF文件里。AOF的好处是最大限度保证数据不丢,坏处是备份文件可能很大,恢复速度也没RDB快。

你也可以在redis.conf里设置AOF:

1
2
3
4
5
6
7
8
# 开启AOF
appendonly yes

# AOF文件名
appendfilename "appendonly.aof"

# 同步策略,这里是每秒同步一次
appendfsync everysec

实际用的时候,你可以根据自己的业务需求选RDB还是AOF,甚至可以两个一起用,既保证数据安全,又兼顾性能。比如,你可以这样设置:

1
2
3
4
5
6
7
8
9
# 开启RDB
save 900 1
save 300 10
save 60 10000

# 开启AOF
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

这样,Redis就会既有RDB的快照,又有AOF的操作日志,双保险,数据更安全!

小结

Redis凭借其飞快的性能和一大票功能,已经成了互联网应用必备的神器。今天咱们主要扒了Redis的几大存储特性:内存存储、数据类型支持、数据淘汰策略和数据持久化。

希望通过这篇文章,你对Redis的存储方式有了更深入的了解。Redis简单易用,但也有很多需要注意的地方,比如数据淘汰策略的选择,持久化方式的权衡等等。

当然,Redis的精彩远不止这些,它还有很多高级特性等着你去发现,比如主从复制、哨兵机制、集群模式等等。这些都是优秀的分布式解决方案,能让你的应用更稳定,更高可用。

让咱们一起潜入Redis的世界,用Redis的力量来武装我们的应用吧!相信有了Redis这个大杀器,你的项目绝对能更上一层楼!还等啥,快去撸代码吧!

本文转载自: 掘金

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

基于Vue3 + FullCalendar实现会议日程预约管

发表于 2024-04-26

最终效果图

日:

日.jpg

周:

周.jpg

月:

月.jpg

一、 FullCalendar插件说明

1. 官网介绍“The Most Popular JavaScript Calendar” 最受欢迎的JavaScript日历!支持 Vue、React、Angular、JavaScript脚本语言。


官网链接:[FullCalendar - JavaScript Event Calendar](https://fullcalendar.io/)


2. 使用时请先下载相关插件:
1
2
3
4
5
npm i @fullcalendar/vue3
npm i @fullcalendar/core
npm i @fullcalendar/daygrid
npm i @fullcalendar/timegrid
npm i @fullcalendar/interaction

本篇中以上插件均使用 “^6.1.9” 版本

下载后通过import 引用即可

1
2
3
4
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'

二、技术梳理

整体分为以下三部分 

1. 左上el-calendar日历部分

当前周的背景色、与FullCalendar日期联动、日历本地化:设置周一为每周的第一天(此处有小坑)。

2. 左下订阅部分

checkbox本身不支持直接修改颜色,通过伪类样式覆盖实现不同颜色展示,再通过修改styleSheet修改checkbox伪类背景色、以及相关业务功能逻辑。

3. 右侧FullCalendar

熟悉fullCalenda相关配置项,按需配置初始视图、语言、固定行数、宽高比等静态结构,自定义周看板,根据相关事件,例如点击事件、滑动选择、拖动事件、等编写相关逻辑。

三、实现方案

1.自定义el-calender

**自定义头部**,通过绑定dateRef手动切换日期月份,同时调用FullCalendar相关方法,确保两个日历日期保持一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<el-calendar v-model="date" ref="dateRef" class="custom-calendar">
<template #header="{ date }">
<div class="w-full flex justify-between">
<span>{{ date }}</span>
<div class="w-20 flex justify-between">
<el-icon class="cursor-pointer" @click="selectDate('prev-month')">
<ArrowLeftBold />
</el-icon>
<el-icon class="cursor-pointer" @click="selectDate('next-month')">
<ArrowRightBold />
</el-icon>
</div>
</div>
</template>
</el-calendar>
1
2
3
4
5
6
7
8
9
// 切换日期月份
const selectDate = value => {
if (!dateRef.value) return
dateRef.value.selectDate(value)
changeDate(date.value)
}

// 同步calendarRef
const changeDate = date => calendarRef.value.getApi().gotoDate(date)
**设置周背景色**,通过watch监视日期变化,当用户选择日期后实时计算此时的周一和周日的日期,处在两者中的日期,添加样式,完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 日历 -->
<el-calendar v-model="date" ref="dateRef" class="custom-calendar">
<template #header="{ date }">
<div class="w-full flex justify-between">
<span>{{ date }}</span>
<div class="w-20 flex justify-between">
<el-icon class="cursor-pointer" @click="selectDate('prev-month')">
<ArrowLeftBold />
</el-icon>
<el-icon class="cursor-pointer" @click="selectDate('next-month')">
<ArrowRightBold />
</el-icon>
</div>
</div>
</template>
<template #date-cell="{ data }">
<p class="w-full h-full flex items-center justify-center"
:class="[data.date >= selectedWeekRange[0] && data.date <= selectedWeekRange[1] ? 'is-week' : '']"
@click="changeDate(data.date)">
{{ data.day.split('-').slice(2).join() }}
{{ data.isSelected ? '✔️' : '' }}
</p>
</template>
</el-calendar>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 周背景色
watch(
date,
(newValue) => {
// 获取用户选择的日期
const selectedDate = new Date(newValue);

// 获取用户选择日期是所在周的第几天(周日为0,周一为1,以此类推)
const dayOfWeek = selectedDate.getDay();

// 计算周一的日期
let monday = new Date(selectedDate);
monday.setDate(selectedDate.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1));

// 计算周日的日期
let sunday = new Date(selectedDate);
sunday.setDate(selectedDate.getDate() + (7 - dayOfWeek) + (dayOfWeek === 0 ? -7 : 0));

selectedWeekRange.value = [monday, sunday];
}, { immediate: true }
)
**日历本地化**,设置周一为周的第一天


 Element Plus官方文档解释说:我们使用 [Day.js](https://day.js.org/docs/en/i18n/i18n) 库来管理组件的日期和时间,例如DatePicker 。 必须在 Day.js 中设置一个适当的区域,以便使国际化充分发挥作用。 您必须分开导入Day.js的区域设置。


所以我们在main.js中加入如下代码
1
2
3
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'dayjs/locale/zh-cn'
 理论上这样就可以了,相信大部分人也都可以,**但是**这里有个坑就是这样可能并不会生效,博主在实验多次后发现,按照如上设置后其他的涵盖日期的组件都会生效,例如日期选择器、时间日期选择器,唯独这个日历不生效.....


最后,需要在上述代码的基础上额外添加
1
2
import dayjs from 'dayjs'
dayjs.locale('zh-cn')
往往困扰几天,自认为很复杂,甚至都打算去扒源码的bug,只需要两行代码就能解决...hh

2.订阅功能

**功能部分,** 主要涉及权限的修改,订阅人的添加与取消,修改颜色,主要为业务逻辑,技术难度相对较低,这里只做展示,不进行展开。

订阅.jpg

**样式部分,** 主要涉及FullCalendar事件背景色,以及checkbox复选框颜色覆盖


 由于checkbox复选框本身不支持颜色的修改,所以我们使用伪类样式对其进行覆盖


 该结构由数据遍历而来,故而我们可以为每个checkbox绑定不同的类名: **:class="`itemBox_${index}`"** ,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<el-collapse-item title="订阅日程" name="second" class="relative z-50 collapseBox" style="height: auto;">
<div class="w-full h-44 overflow-y-scroll overflow-x-hidden z-50">
<div v-for="(item,index) in subscriptionList" :key="item.id">
<div class="flex justify-between items-center mx-4 mb-2" :class="`itemBox_${index}`" v-if="index != 0">
<div class="flex items-center ">
<input type="checkbox" v-model="item.isChecked" class="mr-2" :class="`checked_${item.id}`" @change="filterEvent(item)">
<p class=" text-base">{{ item.followName }}</p>
</div>
<el-icon size="20" color="#54575D" class="cursor-pointer" @click.stop="getSubscriberLoaction(index)">
<MoreFilled />
</el-icon>
<aside v-show="selectedItem == index" class="absolute shadow-deeper rounded-lg rounded-tr-none overflow-hidden" :class="`asideBox_${index}`" style="width: 140px; max-height: 110px; z-index:999999;" @click.stop="null">
<div class="w-full h-full p-4 pt-5 bg-mainWhite">
<div class="mb-4">
<div class="flex justify-between items-center">
<p class="text-test text-base mb-1 cursor-pointer">修改颜色</p>
<el-color-picker v-model="item.color" show-alpha :predefine="predefineColors" @change="changeColor($event,item)" />
</div>
<p class="text-test text-base mb-1 cursor-pointer" @click="setPermission(item)" v-if="item.editable == 1">设置权限</p>
<div class="relative">
<el-popconfirm title="确认取消订阅?" confirm-button-text="是" cancel-button-text="否" :icon="InfoFilled" icon-color="#626AEF" placement="right" @confirm="handleCancleSubscription(item.id)">
<template #reference>
<p class="text-test text-base mb-1 cursor-pointer">取消订阅</p>
</template>
</el-popconfirm>
</div>
</div>
</div>
</aside>
</div>
</div>
</div>
</el-collapse-item>
这样当我们在获取订阅人数据时,通过styleSheets添加伪类样式,为不同的用户添加颜色
1
2
3
4
5
6
7
8
// 正常获取数据,处理数据,储存数据...
// 获取样式表
const styleSheet = document.styleSheets[0];
subscriptionList.value.map(item => {
const rule = `.checked_${item.id}:checked::after {content: "✔" !important; color: ${item.color} ; font-size: 12px; font-weight: bold; border: 2px solid ${item.color}; background-color: white; }`;
// 将生成的规则插入样式表中
styleSheet.insertRule(rule, 0);
})
当用户修改颜色时,我们除了需要向后台发送最新的颜色外,还需要手动更新styleSheets样式(直接刷新用户列表,获取订阅人不会生效,原因为浏览器未刷新,样式表不会更新)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改事件颜色
// 获取最新颜色,发送请求,接口返回200后
// 修改样式
const styleSheet = document.styleSheets[0];
const rules = `.checked_${item.id}:checked::after {content: "✔" !important; color: ${color} !important; font-size: 12px; font-weight: bold; border: 2px solid ${color} !important; background-color: white; }`;
const keys = Object.keys(styleSheet.cssRules);
// 遍历所有样式表,找到原本样式规则进行修改
for (let i = 0; i < keys.length; i++) {
if (styleSheet.cssRules[i].selectorText == `.checked_${item.id}:checked::after`) {
styleSheet.cssRules[i].style.color = `${color} `
styleSheet.cssRules[i].style.border = `2px solid ${color}`
styleSheet.cssRules[i].cssText = rules
// break; 原本是想找到后停止遍历,但是不能写break,会有多条重复的样式表造成不生效
}
}

3.FullCalendar的使用

**FullCalendar的内置函数及配置项**
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
// 切换到下一月/周/日
this.$refs.FullCalendar.getApi().next()
// 切换到上一月/周/日
this.$refs.FullCalendar.getApi().prev()
// 跳转到今天
this.$refs.FullCalendar.getApi().today()
// 跳转到指定日期 formatData是日期 格式为 yyyy-MM-dd
this.$refs.FullCalendar.getApi().gotoDate(formatData)
// 获得当前视图起始位置的日期
this.$refs.FullCalendar.getApi().getDate()
// 获得当前视图 里面有一些参数
this.$refs.FullCalendar.getApi().view
// 当前视图的类型
this.$refs.FullCalendar.getApi().view.type
// 当前显示的事件(日程)的开始时
this.$refs.FullCalendar.getApi().view.activeStart
// 当前显示的事件(日程)的结束时
this.$refs.FullCalendar.getApi().view.activeEnd
//访问当前视图所涉及的日历对象或者日历配置信息。
this.$refs.FullCalendar.getApi().view.calendar
// 获得当前所显示的所有事件(日程)
this.$refs.FullCalendar.getApi().view.calendar.getEvents()
// 向日历中添加事项
this.$refs.FullCalendar.getApi().view.calendar.addEvent({
id: '001',
title: `青兔_test01`,
start: '2024-04-25' + ' 13:00:00',
end: '2024-04-25' + ' 17:00:00',
// 修改背景颜色
backgroundColor:'#d8377a',
// 修改边框颜色
borderColor:'#d8377a',
})

更多更详细FullCalendar介绍可查询官方文档,本篇只展示当前功能下所需要的相关配置

官网链接:[FullCalendar - JavaScript Event Calendar](https://fullcalendar.io/)


**配置FullCalendar,** 了解相关配置项,本篇中使用的配置如下
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
// fullCalendar 配置项
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin], //需要加载的插件
initialView: "timeGridDay", //初始视图
height: "780px",
locale: zhcn, //语言汉化
selectable: true,
editable: true,
forceEventDuration: true,
// droppable: false,
// dropAccept: ".eventListItems", //可被拖进
dayMaxEventRows: 99, //事件最大展示列数
nowIndicator: true,
fixedWeekCount: false, //因为每月起始日是星期几不固定,导致一个月的行数会不固定,是否固定行数
// drop: null, //外部拖拽进的事件方法
handleWindowResize: true,
windowResizeDelay: 100,
allDaySlot: false, // 关闭全天选项
aspectRatio: 2, //宽高比
// 最小时间
slotMinTime: '06:00:00',
// 最大时间
slotMaxTime: '22:30:00',
customButtons: {
myCustomButton: {
text: '看板',
click: function() {
isWeekViewShow.value = true
}
}
},
headerToolbar: {
left: "today prev next",
center: "title",
right: "myCustomButton,dayGridMonth,timeGridWeek,timeGridDay"
}, //日历上方的按钮和title
events: matchList.value, //绑定展示事件
// 自定义日程展示内容
// eventContent: event => {},
eventDidMount: (info) => {},
//点击日期info是单元格的信息
dateClick: info => {},
//事件的点击
eventClick: info => {},
// 移动事件或者拓展事件时间触发函数 返回数组 item._context.options.events Array 当前所有事件
eventsSet: info => {},
// 滑动选择时触发
select: info => {},
// 时间调整结束后触发
eventResize: info => {},
// 拖动日程触发
eventDrop: info => {},
// 切换视图时触发
datesSet: view => {},
});
**同步日期,** el-calendar与fullCalendar需要双向绑定,前面点击小日历切换日期时同时修改日程日期,现使用FullCalendar内置函数对小日历进行日期绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
onMounted(() => {
nextTick(() => {
// calendar日期同步到日历
document.querySelector('.fc-today-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});
document.querySelector('.fc-prev-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});

document.querySelector('.fc-next-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});
// 绑定事件
document.addEventListener('click', handleClickOutside);
})
})

相关业务事件, 例如用户滑动选择、拖拽日程边缘增加/减少时间、拖动日程修改日程在对应方法中执行相关业务逻辑即可,对应的回调传递info参数,在info.event中可以获取用户执行后的开始时间和结束时间,需要强调的是,此时获取的时间为一个表示日期和时间的 ISO 8601 格式的字符串,我们需要把他转换成我们需要的YYYY-MM-DD HH:mm格式,封装formatDateTime方法。

1
2
3
4
5
6
7
8
9
10
// 格式化日期
const formatDateTime = (isoString) => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//点击日期info是单元格的信息
dateClick: info => {},
//事件的点击
eventClick: info => {
// 业务逻辑...
},
eventsSet: info => {},
// 滑动选择时触发
select: info => {
// 处理时间数据
const startDate = formatDateTime(info.start)
const endDate = formatDateTime(info.end)
// 业务逻辑...
},
// 时间调整结束后触发
eventResize: async info => {
resizeEventDate(info)
},
// 拖动日程触发
eventDrop: async info => {
resizeEventDate(info)
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const resizeEventDate = async info => {
const resizeEvent = matchList.value.find(item => item.id == info.event.id)
resizeEvent.startTime = formatDateTime(info.event.start)
resizeEvent.endTime = formatDateTime(info.event.end)
const result = await submit(resizeEvent)
if (result.code == 200) ElMessage.success('修改成功')
else ElMessage.error('修改失败')
const timeObj = {
startTime: '',
endTime: ''
}
timeObj.startTime = date2Str(info.view.activeStart)
timeObj.endTime = date2Str(info.view.activeEnd)
myMatchList(timeObj) // 修改成功后重新获取数据
}
**切换视图,** 通过内置方法datesSet中的view参数,可以获得当前视图是日期范围,**需要强调的是,返回的结束时间为后一天的零天,例如周时间为2023-12-11 至2023-12-17,实际返回的结果为2023-12-11T00:00:00+08:00" 至 "2023-12-18T00:00:00+08:00",** 所以我们需要修改结束时间。
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
// 切换视图时触发
datesSet: view => {
timeObj.startTime = date2Str(view.start)
// 结束时间返回为后一天的零点 例如周时间应为2023-12-11 至 2023-12-17
// 实际返回结果为2023-12-11T00:00:00+08:00" 至 "2023-12-18T00:00:00+08:00"
// 修改结束时间
const timeTemp = new Date(date2Str(view.end)).getTime() - 86400000 // 减一天后的时间戳
timeObj.endTime = date2Str(new Date(timeTemp))
// 周视图-领导视图交互及数据处理
if ((view.end.getTime() - view.start.getTime()) / 1000 / 3600 / 24 === 7) {
calendarOptions.headerToolbar.right = 'myCustomButton dayGridMonth,timeGridWeek,timeGridDay'
weekViewColumn.value = [{ key: 'ownUserName', width: 100 }]
for (let d = 0; d < 5; d++) {
let day = new Date(view.start.getTime() + (24 * 3600 * 1000 * d))
weekViewColumn.value.push({
key: date2Str(day),
title: `${weekDay[d]}/${day.getMonth() + 1}-${day.getDate()}`
})
}
// weekViewData.value
} else {
calendarOptions.headerToolbar.right = 'dayGridMonth,timeGridWeek,timeGridDay'
}
myMatchList(timeObj)
// 记录视图
submitView({ view: view.view.type })
},

4.周看板

周看板为单独封装在FullCalendar上的,非FullCalendar原生自带功能。


主要需求是为了帮助领导助理及时安排、记录、处理领导的日程,方便规划领导整体行程。


**效果图**

看板.jpg

使用FullCalendar中的 **customButtons**配置项,添加看板按钮
1
2
3
4
5
6
7
8
customButtons: {
myCustomButton: {
text: '看板',
click: function() {
isWeekViewShow.value = true
}
}
},
看板列表与订阅人列表相同,在获取到事件数据后,对事件进行处理
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
 // 处理周视图弹窗数据--以 ownUser 维度组合
if (item.ownUser) {
let eIdx = weekViewData.value.findIndex(el => el.ownUser == item.ownUser)
if (eIdx === -1) {
weekViewData.value.push({
ownUser: item.ownUser,
ownUserName: item.ownUserName,
[item.startTime.slice(0, 10)]: [{
subject: item.subject,
id: item.id
}]
})
} else {
if (Object.hasOwn(weekViewData.value[eIdx], item.startTime.slice(0, 10))) {
weekViewData.value[eIdx][item.startTime.slice(0, 10)].push({
subject: item.subject,
id: item.id
})
} else {
weekViewData.value[eIdx][item.startTime.slice(0, 10)] = [{
subject: item.subject,
id: item.id
}]
}
}
// 处理跨天日程
if (calcDays(item.startTime, item.endTime) > 0) {
for (let n = 1; n <= calcDays(item.startTime, item.endTime); n++) {
weekViewData.value[eIdx][getNextDay(item.startTime, n)] = [{
subject: item.subject,
id: item.id
}]
}
}
}
在切换视图时处理相关数据
1
2
3
4
5
6
7
8
9
10
11
// 周视图-领导视图交互及数据处理
if ((view.end.getTime() - view.start.getTime()) / 1000 / 3600 / 24 === 7) {
calendarOptions.headerToolbar.right = 'myCustomButton dayGridMonth,timeGridWeek,timeGridDay'
weekViewColumn.value = [{ key: 'ownUserName', width: 100 }]
for (let d = 0; d < 5; d++) {
let day = new Date(view.start.getTime() + (24 * 3600 * 1000 * d))
weekViewColumn.value.push({
key: date2Str(day),
title: `${weekDay[d]}/${day.getMonth() + 1}-${day.getDate()}`
})
}
静态结构 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<el-dialog v-model="isWeekViewShow" width="60%" class="week-view-dialog">
<template #header>
<h1>看板</h1>
</template>
<div class="dialog-content">
<el-table :data="weekViewData" border stripe show-overflow-tooltip height="680">
<el-table-column v-for="col in weekViewColumn" :prop="col.key" :key="col.key" :label="col.title" :width="col.width">
<template #default="scope">
<div style="min-height: 63px;" :class="col.key == 'ownUserName' ? 'flex items-center' : ''">
<p v-if="col.key == 'ownUserName'" style="">{{ scope.row[col.key] }}</p>
<ul v-else>
<li v-for="(li, idx) in scope.row[col.key]" class="text-ellipsis whitespace-nowrap overflow-hidden">
<el-tooltip :content="li.subject" placement="top" effect="light">
{{ `${idx + 1}.${li.subject}` }}
</el-tooltip>
</li>
</ul>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>

四、总结

以上就是基于Vue3 + FullCalendar实现会议日程预约管理系统开发方案,在本方案中,我们对el-calendar的二次开发,订阅功能的实现,FullCalendar的技术说明等等...内容过多,所以有些地方并没有详细说明,若您有什么疑问或对我的内容进行指正,欢迎您在下方进行评论探讨。

希望本篇内容对您有所帮助。

本文转载自: 掘金

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

使用 CXYTableViewExt 来简化 UITable

发表于 2024-04-26

CXYTableViewExt

CXYTableViewExt 是用来简化 UITableView - Cell 配置的库。特别是对于拥有多种类型的 Cell,逻辑上将更加清晰,所见即所得。

几乎在你用到 UITableView 的地方,都可以使用 CXYTableViewExt 来简化代码和逻辑。

开始使用

1、简单使用,无需设置 dataSource 和 delegate,也不需要注册 Cell


如需实现上面的UI界面:

1、让 ArrowTextCell 实现 CXYTableItemProtocol 的协议,Cell 可以是纯代码 也可以是 Xib。

1
2
3
4
5
6
7
8
9
objc复制代码@interface ArrowTextCell ()<CXYTableItemProtocol>
@end
@implementation ArrowTextCell

#pragma mark - CXYTableItemProtocol
// 配置你的 Cell data
- (void)configData:(id)data {
self.title.text = data;
}

2、类似 Masonry block 写法, 内部会自动帮你注册Cell,设置默认代理对象和数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
objc复制代码// ViewController.m 
- (void)bindViewData {

[self.tableView.t makeItems:^(CXYTable * _Nonnull make) {

[make addCellClass:ArrowTextCell.class data:@"设置页示例" didSelectBlock:^(id  _Nonnull data, NSIndexPath * _Nonnull indexPath) {
// 处理点击
}];

[make addCellClass:ArrowTextCell.class data:@"资讯列表示例" didSelectBlock:^(id  _Nonnull data, NSIndexPath * _Nonnull indexPath) {

}];

[make addCellClass:ArrowTextCell.class data:@"Section Header & Footer 示例" didSelectBlock:^(id  _Nonnull data, NSIndexPath * _Nonnull indexPath) {

}];
}];

}

2、类设置页 - delegate 方式让 ViewController 响应 Cell 的操作


1、有时我们需要响应 Cell 里的一些动作,我们可以在配置 Cell 时,给它设置一个代理对象(delegate)

1
objc复制代码- (void)addCellClass:(Class)cellClass data:(id)data delegate:(id)delegate

2、在 Cell 中定义一些代理对象( delegate)需要实现的协议

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
objc复制代码@protocol SwitchCellDelegate <NSObject>
- (void)switchCellDelegateSwitchChanged:(id)data;
@end

@interface SwitchCell ()<CXYTableItemProtocol>
@property (nonatomic, weak) id delegate;
@property (nonatomic, strong) SwitchModel *model;
@end

@implementation SwitchCell

- (IBAction)onSwitch:(UISwitch*)sender {
// 开关操作
if ([self.delegate respondsToSelector:@selector(switchCellDelegateSwitchChanged:)]) {
[self.delegate switchCellDelegateSwitchChanged:self.model];
}
}

#pragma mark - CXYTableItemProtocol
- (void)configData:(SwitchModel*)data indexPath:(NSIndexPath *)indexPath delegate:(id)delegate {
self.model = data;
self.delegate = delegate; // 接收代理对象
...
}

@end

3、让这个代理对象遵循你的协议,从而将 Cell 的动作传递给 delegate 响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
objc复制代码- (void)bindViewData {

[self.tableView.t makeItems:^(CXYTable * _Nonnull make) {
[make addCellClass:HeadTitleCell.class data:@"通知"];
// delegate: self 实现 cell 里面的交互
[make addCellClass:SwitchCell.class data:[SwitchModel title:@"系统消息通知" isOn:YES] delegate:self];
[make addCellClass:SwitchCell.class data:[SwitchModel title:@"通知显示消息详情" isOn:NO] delegate:self];
[make addCellClass:SwitchCell.class data:[SwitchModel title:@"振动" isOn:YES] delegate:self];

[make addCellClass:HeadTitleCell.class data:@"提示音"];
[make addCellClass:ArrowTextCell.class data:@"消息提示音" didSelectBlock:^(id  _Nonnull data, NSIndexPath * _Nonnull indexPath) {
// 处理点击
}];
[make addCellClass:ArrowTextCell.class data:@"来电铃声" didSelectBlock:^(id  _Nonnull data, NSIndexPath * _Nonnull indexPath) {

}];
}];
}

#pragma mark - SwitchCellDelegate
- (void)switchCellDelegateSwitchChanged:(SwitchModel*)data {
data.isOn = !data.isOn;
NSLog(@"%@ %@",data.title, @(data.isOn));
}

3、列表 - 下拉刷新或上拉加载更多


每次你的数据变化时,直接重新绑定数据就行。如果你觉得性能有影响,那可以使用 CXYTable.h 里的一些插入、添加、更新、删除方法来实现更新。

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
objc复制代码- (void)requestPage:(NSInteger)page {
// 模拟请求,假数据
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *res = @[].mutableCopy;
for (NSInteger i=0; i<10; i++) {
NewsModel *model = [NewsModel new];
model.title = @"新闻标题";
model.desc = @"新闻摘要";
model.img = @"cover";
[res addObject:model];
}

if (page==1) {
self.list = res;
} else {
self.list = [self.list arrayByAddingObjectsFromArray:res];
}
self.page = page;
[self bindViewData];
});
}

- (void)bindViewData {
[self.tableView.t makeItems:^(CXYTable * _Nonnull make) {
[make addCellClass:NewsCell.class dataList:self.list];
}];
}

4、拥有 Header & Footer


同样,你的 Header 和 Footer 也需要实现 CXYTableItemProtocol 的协议。从配置项来看,几乎是所见即所得的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
objc复制代码- (void)bindViewData {
NSArray *sectionList1 = @[@"1",@"2",@"3",@"4",@"5"];

[self.tableView.t makeItems:^(CXYTable * _Nonnull make) {
[make addHeaderItem:Header.class data:@"我是Header1"];
[make addCellClass:TextCell.class dataList:sectionList1];
[make addFooterItem:Footer.class data:@"我是Footer1"];

[make addHeaderItem:Header.class data:@"我是Header2"];
[make addCellClass:TextCell.class data:@"text-cell"];
[make addCellClass:TextCell.class data:@"text-cell"];
[make addFooterItem:Footer.class data:@"我是Footer1"];
}];
}

更多操作

在 CXYTable.h 文件里面提供了很多操作方法:

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
objc复制代码// 可使用自定义的
- (void)configDataSource:(id<UITableViewDelegate,UITableViewDataSource>)dataSource;

// 默认的数据源和代理
- (void)useDefaultDataSource;
// 使用默认的数据源和代理,cell被点击时调用
- (void)defaultDidSelectCell:(void(^)(UITableView *tableView, NSIndexPath *indexPath))didSelect;

// 配置item
- (void)makeItems:(void(NS_NOESCAPE ^)(CXYTable *make))block;
- (void)updateItems:(void(NS_NOESCAPE ^)(CXYTable *make, UITableView *tableView))block;
- (void)removeItems:(void(NS_NOESCAPE ^)(CXYTable *make, UITableView *tableView))block;

/**
添加item
*/
- (void)addCellClass:(Class)cellClass data:(id _Nullable)data;
...

/**
插入item
*/
- (void)insertCellItem:(Class)cellClass data:(id _Nullable)data indexPath:(NSIndexPath *)indexPath;
...

/**
删除 item
*/
- (void)removeCellItem:(NSIndexPath *)indexPath;
...

/**
获取item 数据
*/
// header item
- (id)headerItemDataInSection:(NSUInteger)section;
...

// footer item
- (id)footerItemDataInSection:(NSUInteger)section;
...

// cell item
- (id)cellItemDataAtIndexPath:(NSIndexPath*)indexPath;
...

Tip: 有些第三方统计SDK可能会 hook UITableView,默认给 tableView.delegate 一个对象。这会影响到 CXYTableViewExt 里内部设置默认数据源判断,这时,你需要手动设置默认的数据源和代理。

1
objc复制代码[self.tableView useDefaultDataSource];

源码:

OC版: CXYTableViewExt-OC

Swift版:CXYTableViewExt-Swift

本文转载自: 掘金

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

Spring Boot项目实战:消息丢失和重复消费问题

发表于 2024-04-26

你好,我是田哥

在我的充电桩项目中,有个用户积分模块,原型图如下:

图片

我的积分

下面来聊聊这个项目中,积分增加场景。

  • 用户充值完成后,赠送积分,比如:充100元,给用户积分新增100个。
  • 用户充电支付完成(非余额支付),赠送积分。
  • 用户邀请新人注册,赠送积分
  • 新用户认证完成,赠送积分
  • ….

关于积分增加策略,基本上都是由运营来定,总之,很多项目中都有这么个功能。

常规系统就是用户上面的行为伴随着用户积分处理完成(在同一个事物里),但,咱们为了提升系统性能和用户体验,我们通常把积分增加这类业务采用异步方式去实现。

比如:线程池、各种消息队列等

在我们这个版本中,采用的是RabbitMQ消息队列来实现的。

问题

既然使用RabbitMQ,那我们就不得不考虑关于消息的问题:

  • 消息丢失问题
  • 重复消费问题

消息丢失问题

这里对用户积分增加,如果把消息搞丢了,用户积分最终并不会得到增加,那用户肯定不干了,为了防止这类问题出现,我们采用下面的解决方案。

1:我们采用confirm模式+持久化+手动ack

2:消息丢失种鸽问题:消息发送失败,我们采用失败消息记录表

3:定时任务轮训失败消息记录,再次发送

这里为了防止多次重试问题,所以设置一个重试上限,并加入警告(比如一条消息最多重发5次,一旦到5次了,就给运维/开发/测试发送邮件警告)。

重复消费问题

这里是对用户积分增加,所以,绝对不能重复消费,不然这样会导致用户积分暴增,数据会出现一致性问题。解决:

1:每个消息有一个唯一的reqId,reqId=业务前缀+UUID+年月日时分秒毫秒的时间戳

2:在对比是否重复消费之前,对用户加上分布式锁,key=固定用户分布式锁前缀+userId

整体流程图

标配版:

图片

标配版

为了更好地监控消息发送失败问题,我们还可以对标配版进行升级。

图片

升级版

其他问题

我们上面说了,为了防止消息丢失,采用confirm模式+持久化+手动ack

但,实现起来并非那么简单,如果没有做过,很多东西是无法体会到的。

在使用confirm模式时,新的问题来了。

图片

问题

我们Spring中,一个Bean默认是单列的,这样的话会造成一个RabbitMQTemplate只能绑定一个confirm,这就不对了,我们需要RabbitMQTemplate不受Spring这个影响,很多人第一印象想到的就是采用原型模式。也就是在bean上添加注解:@Scope(“prototype”) 但,问题来了,比如在一个producer bean里注入RabbitMQTemplate,他最终还是认为你这个RabbitMQTemplate是单列,又和上面原型违背了,网上很多办法是给这个producer也搞成原型模式。

这个确实能解决这个问题。

说白了就是 从请求开始的bean开始到最后发送消息,这个过程的bean要都是原型模式才行。

比如:controller–service–producer

挖了个蛐蛐,问题又来了,项目中定时任务采用的是xxl-job,它的每个job都必须是单列的,上面的办法又不行了。

绝招:用Spring中的ApplicationContext**的getBean方法直接获取对应的Bean就不存在问题。

这里有点绕哈,说白了就是必须使用原型,不能使用单例。

核心代码

定义原型的rabbitTemplate。

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Configuration
public class RabbitConfig {
    //省略部分非核心代码
    @Bean
    @Scope("prototype")
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory); 
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }
}

confirm模式

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复制代码@Component
public class UserPointProducer { 
    @Resource
    private RetryMessageMapper chargeUserMapper;

    public void sendMessage(String message) {
        RabbitTemplate rabbitTemplate = ApplicationContextFactory.getBean(RabbitTemplate.class);
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.info("UserPointConfirm ConfirmCallback 关联数据:{},投递成功,确认情况:{}", correlationData, ack);
            } else {
                RetryMessage retryMessage = new RetryMessage();
                retryMessage.setContent(message);
                retryMessage.setRetry(5);
                retryMessage.setCreateTime(new Date());
                retryMessage.setStatus(0);
                retryMessage.setRetriedTimes(0);
                retryMessage.setType(RabbitMQConstantEnum.USER_POINT.getType());
                chargeUserMapper.insert(retryMessage);
                log.info("UserPointConfirm ConfirmCallback 关联数据:{},投递失败,确认情况:{},原因:{}", correlationData, ack, cause);
            }
        });
 
        //后面+1 主要是为了掩饰消息发送失败
        rabbitTemplate.convertAndSend(RabbitMQConstantEnum.USER_POINT.getExchange()+"1"
                , RabbitMQConstantEnum.USER_POINT.getRoutingKey(), message, message1 -> {
                    message1.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 设置消息持久化
                    return message1;
                }, correlationId);
    }
}

下面来写个测试发送案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码/**
 * {@code @description:} 测试案例
 *
 * @author tianwc 公众号:Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章:https://woaijava.cc/
 * {@code @date:} 2024-03-24 9:19
 * {@code @version:} 1.0
 */
@GetMapping("/send")
public void send() {
    UserPointMessage userPointMessage = new UserPointMessage();
    userPointMessage.setType(UserUpdatePointEnum.ADD.getType());
    userPointMessage.setUserId(1L);
    userPointMessage.setPoint(9);
    userPointMessage.setReqId(MessageReqIdPrefixConstant.USER_POINT + UUID.randomUUID()+ DateUtils.formatDefaultDateMs());
    userPointProducer.sendMessage(JSON.toJSONString(userPointMessage));
}

消费者:

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
less复制代码/**
 * {@code @description:} 用户积分消息消费者
 *
 * @author tianwc 公众号:Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章:<a href="https://woaijava.cc/">博客地址</a>
 * {@code @date:} 2024-03-24 9:19
 * {@code @version:} 1.0
 */
@RabbitListener(queues = "user.point.queue")
@Component
@Slf4j
public class UserPointConsumer {

    @Resource
    private UserPointService userPointService;

    @RabbitHandler
    public void process(Object data, Channel channel, Message message) throws IOException {
        try {
            log.info("消费者接受到的消息是:{},消息体为:{}", data, message);
            UserPointMessage userPointMessage = JSON.parseObject(new String(message.getBody()), UserPointMessage.class);
            userPointService.updateUserPoint(userPointMessage);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception exception) {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

具体业务逻辑实现:

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
less复制代码/**
 * {@code @description:} 用户积分扣除消费者服务实现类
 *
 * @author tianwc 公众号:Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章:<a href="https://woaijava.cc/">博客地址</a>
 * {@code @date:} 2024-03-24 9:19
 * {@code @version:} 1.0
 */
@Slf4j
@Service
public class UserPointServiceImpl implements UserPointService {
    @Resource
    private ChargeUserMapper chargeUserMapper;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private PointsChangeRecordMapper pointsChangeRecordMapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateUserPoint(UserPointMessage userPointMessage) {
        RLock lock = redissonClient.getLock(RedisConstantPre.USER_INFO_ID_PRE + userPointMessage.getUserId());
        lock.lock();
        try {
            int count = pointsChangeRecordMapper.selectByReqId(userPointMessage.getReqId());
            if (count > 0) {
                log.info("消息体参数 【重复消息】:{}", userPointMessage);
                return;
            }
            PointsChangeRecord pointsChangeRecord = new PointsChangeRecord();
            pointsChangeRecord.setUserId(userPointMessage.getUserId());
            pointsChangeRecord.setPoint(userPointMessage.getPoint());
            pointsChangeRecord.setType(userPointMessage.getType());
            pointsChangeRecord.setCreateTime(new Date());
            pointsChangeRecord.setReqId(userPointMessage.getReqId());
            //积分变动记录
            pointsChangeRecordMapper.insert(pointsChangeRecord);

            ChargeUser chargeUser = chargeUserMapper.selectByPrimaryKey(userPointMessage.getUserId());
            if (userPointMessage.getType() == UserUpdatePointEnum.ADD.getType()) {
                chargeUser.setPoint(chargeUser.getPoint() + userPointMessage.getPoint());
            } else {
                chargeUser.setPoint(chargeUser.getPoint() - userPointMessage.getPoint());
            }
            //用户积分变动
            chargeUserMapper.updateByPrimaryKey(chargeUser);
        } finally {
            lock.unlock();
        }
    }
}

这里的分布式锁的好处:

  • 保证了这个重复消费部分代码的原子性
  • 保证了此时只有一个线程对用户积分进行修改

其实,正常情况下,不会走失败消息记录表,但是作为程序不得不多考虑点。

定时任务部分代码:

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
java复制代码/**
 * {@code @description:} 用户积分消息发送job
 *
 * @author tianwc 公众号:Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章:https://woaijava.cc/
 * {@code @date:} 2024-03-24 9:19
 * {@code @version:} 1.0
 */
@Slf4j
@Component
public class UserPointRetryMessageJob {

    @Resource
    private RetryMessageMapper retryMessageMapper;
    @Resource
    private UserPointProducer userPointProducer;

    @XxlJob("userPointRetryMessageJob")
    public void process() {
        log.info("开始执行 userPointRetryMessageJob 定时任务");
        XxlJobHelper.log("start userPointRetryMessageJob job");
        int countRetryMessage = retryMessageMapper.countRetryMessage(0, 0);
        if (countRetryMessage == 0) {
            log.info(" 执行结束 userPointRetryMessageJob 没有消息需要重发");
        }
        List<RetryMessage> retryMessages = retryMessageMapper.selectRetryMessage(0, 0);
        for (RetryMessage retryMessage : retryMessages) {
            userPointProducer.sendMessage(retryMessage);
        }
    }
}

这里还可以优化,你能想到吗?

List<RetryMessage> retryMessages = retryMessageMapper.selectRetryMessage(0, 0);

这里是一次性全部查出来了,如果出现大量消息发送失败,一次性放到本地缓存里,很容易出问题,所以,我们可以再优化成分页进行处理,比如:每次处理50条,再根据count来计算需要进行分页。

定时任务中生产者代码实现:

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
scss复制代码/**
 * {@code @description:} 用户积分消息发送生产者
 *
 * @author tianwc 公众号:Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章:https://woaijava.cc/
 * {@code @date:} 2024-03-24 9:19
 * {@code @version:} 1.0
 */
@Slf4j
@Component
public class UserPointProducer {
    @Resource
    private RetryMessageMapper chargeUserMapper;

    public void sendMessage(RetryMessage retryMessage) {
        log.info("用户积分消息重试补发,{}", retryMessage);
        String message = retryMessage.getContent();
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        RabbitTemplate rabbitTemplate = ApplicationContextFactory.getBean(RabbitTemplate.class);
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                retryMessage.setStatus(1);
                chargeUserMapper.updateByPrimaryKey(retryMessage);
                log.info("UserPointConfirm ConfirmCallback 关联数据:{},投递成功,确认情况:{}", correlationData, ack);
            } else {
                retryMessage.setRetriedTimes(retryMessage.getRetriedTimes() + 1);
                chargeUserMapper.updateByPrimaryKey(retryMessage);
                log.info("UserPointConfirm ConfirmCallback 关联数据:{},投递失败,确认情况:{},原因:{}", correlationData, ack, cause);
            }
        });

        rabbitTemplate.convertAndSend(RabbitMQConstantEnum.USER_POINT.getExchange()
                , RabbitMQConstantEnum.USER_POINT.getRoutingKey(), message, message1 -> {
                    message1.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 设置消息持久化
                    return message1;
                }, correlationId);
        log.info("用户积分消息重试补发完成");
    }
}

失败消息表

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `retry_message` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `type` int DEFAULT NULL,
  `content` text,
  `retried_times` int DEFAULT NULL,
  `retry_limit` int NOT NULL DEFAULT '1',
  `create_time` datetime DEFAULT NULL,
  `status` int DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ;

一个数据库里就只要一张表即可,专门用来存储发送失败消息。

tyep:什么业务场景

content:具体消息内容

retried_times:已经重试了几次

retry_limit:重试次数限制

status:状态,是否需要重试

这里的重试次数限制,我们也可以采用配置的方式,这样就可以在分布式配置中心对此进行动态调整,不过,这个好像没什么必要,因为不会频繁地更换这个限制。直接存在表里还可以动态的针对某些业务做特殊处理,比如业务A限制次数2次,业务B次数改成3次….

没有完美的解决方法,但总有相对完美的解决方案即可。

关于积分模块,其实不止有增加积分,还有扣除积分。

比如:用户使用积分兑换优惠券,积分目前设计在用户中心,优惠券又在营销中心,所以,会涉及到分布式事务问题。

我们可以采用Seata、Atomikos、RockSeataetMQ等技术来解决,目前充电桩项目中用到过Atomikos,但是代码量实在是会增加不少,最后使用了Seata来解决分布式事务问题。

最后

希望通过本文学习,下次再遇到面试官问消息队列的两个问题,就不再是背八股文了。

好了,今天就跟大家分享这么多,希望能给你带来点点帮助。

麻烦个三连呗:点赞、转发、再看,谢谢啦!

本文转载自: 掘金

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

1…101112…399

开发者博客

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