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

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


  • 首页

  • 归档

  • 搜索

Redis实战-session共享 添加依赖 配置文件 启用

发表于 2021-07-03

创建一个使用了srping的mavne项目,这里我们借助spring-session来实现。

添加依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码         <!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis-session-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

配置文件

1
2
3
4
5
6
7
8
bash复制代码#地址
spring.redis.host=
#端口
spring.redis.port=
#密码
spring.redis.password=
#库号
spring.redis.database=

启用Redis Http会话

1
2
3
4
less复制代码@Configuration
@EnableRedisHttpSession
public class RedisSessionManager {
}

项目到这里就已经实现了Redis管理session了。

使用分析

接下来我们一起分析一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码    @RequestMapping()
public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
//现在操作的都是redis里面的session
HttpSession session = request.getSession();
List<String> list = (List<String>) session.getAttribute("list");
if (list == null){
list = new ArrayList<>();
//放入redis
session.setAttribute("list",list);
}
list.add("y2m");
response.getWriter().println("size:"+list.size());
response.getWriter().println("sessionId:"+session.getId());
}

输出结果: 当你第一次访问的时候 会输出 size:1 ,第二次访问的时候 会输出 size:2, 第三次访问的时候 会输出 size:2 ,以后无论你访问多少次 都是输出 size:2

结果解读:

当你第一次访问的时候这时候redis并没有session,得到的session是服务器的session对象及Java对象,这时候自然输出 size:1

当你第二次访问的时候这时候redis有session,得到的session是从redis里面拿到 list的size为1 进行add操作 size变为2 此时你并没有把新的list更新到redis的list,所以redis的list的size还是1

往后都是这样…………

思考:那为什么不适用redis的session管理,下面的代码就有效呢?因为JVM管理的session在堆中的地址是不会变的 所以list在堆中的地址也不会变

代码优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码    /**
* 解决楼上的方法就是 session每发生一次变化就放入redis,同步内容
* @param request
* @param response
* @throws IOException
*/
@RequestMapping()
public void optimization(HttpServletRequest request, HttpServletResponse response) throws IOException {
//现在操作的都是redis里面的session
HttpSession session = request.getSession();
List<String> list = (List<String>) session.getAttribute("list");
if (list == null){
list = new ArrayList<>();
//放入redis
session.setAttribute("list",list);
}
list.add("y2m");
//放入redis 同步
session.setAttribute("list",list);
response.getWriter().println("size:"+list.size());
response.getWriter().println("sessionId:"+session.getId());
}
1
2
scss复制代码    //移除session redis管理的话到redis移除
request.getSession().invalidate();

本文转载自: 掘金

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

给掘金做了一个数据统计分析工具 Plus 版

发表于 2021-07-03

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

本文只讲分析,数据来源请看 给掘金做了一个数据统计分析工具

前言

在原有基础上增加如下功能

  1. 今日升级名单
  2. 近3日7日30日升级名单
  3. 近3日7日30日浏览Top10
  4. 近3日7日30日获赞Top10
  5. 热门作者,每日数据折线图

看效果

  1. 今日升级名单
    image.png
  2. 近3日7日30日升级名单
    image.png
  3. 近3日7日30日浏览Top10
    image.png
  4. 近3日7日30日获赞Top10
    image.png
  5. 热门作者,每日数据折线图
      犹豫不能嵌入html 想看实际效果 ->点我
    image.png

分析数据

代码例子使用的还是 scala

主要代码都有注释,有疑问欢迎评论提问

想要数据,可以评论找我要,也可以自己采集(给掘金做了一个数据统计分析工具)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
scala复制代码
import cn.hutool.core.io.IoUtil
import cn.hutool.core.lang.TypeReference
import cn.hutool.json.JSONUtil
import com.yeting.juejin.JueLI.Author

import java.io.{FileInputStream, FileOutputStream}
import java.lang.reflect.Type
import java.nio.charset.StandardCharsets
import java.time.{LocalDate, LocalDateTime}
import java.time.format.DateTimeFormatter
import java.util
import scala.collection.JavaConverters._
import scala.collection.{immutable, mutable}
import scala.math.Ordering

object J {

val dateFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm")
val yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd")
val dateFormatOut = DateTimeFormatter.ofPattern("MM-dd HH:mm")

val map: mutable.Map[String, List[Author]] = mutable.ListMap()

def main(args: Array[String]): Unit = {
//加载数据
load()
//top榜单
top
//图表
userReport
}

private def userReport = {
//按照每天分组
val dayGroup = map.toList
.map(t => {
(LocalDateTime.parse(t._1, dateFormat).format(yyyyMMdd), t._2)
})
.groupBy(_._1)

//取到表格下面的日期
val xAxisdata = dayGroup.keys.toList.sortBy(t => t.toInt).map(t => s"'${t}'").mkString(",")

//这里是 用户角度 转换成 每一天多少赞
val userGroup = dayGroup.flatMap(t => {
val authors: immutable.Iterable[(String, String, String, Int)] = t._2
.flatMap(t => {
t._2
})
.groupBy(_.getUser_id)
.map(m => {
val authorList: List[Author] = m._2.sortBy(_.getTime)
(m._1, t._1, authorList.head.getUser_name, authorList.last.getGot_digg_count.toInt - authorList.head.getGot_digg_count.toInt)
})
authors
})
.groupBy(_._1)
//这里必须过滤一些,不然人太多了,直接爆炸
.filter(
m => {
m._2.map(t => {
t._4
}).sum > 50
})
.values
//这里排序,方便表格好找
.toList.sortBy(_.map(t => t._4).sum)(Ordering.Int.reverse)

//取到表格展示的所有用户
val legendData = userGroup.map(t => s"'${t.head._3}'").mkString(",")

//组装表格每行数据
val series = userGroup
.map(t => {
s"""
|{
| name: '${t.head._3}',
| type: 'line',
| data: [${t.toList.sortBy(_._2).map(_._4).mkString(",")}]
|}
|""".stripMargin
}).mkString(",")

//组装html
val html =
s"""
|<!DOCTYPE html>
|<html style="height: 100%">
| <head>
| <meta charset="utf-8">
| </head>
| <body style="height: 100%; margin: 0">
| <div id="container" style="height: 100%"></div>
| <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
| <script type="text/javascript">
| var dom = document.getElementById("container");
| var myChart = echarts.init(dom);
| var app = {};
| var option;
| option = {
| title: {
| text: ''
| },
| tooltip: {
| trigger: 'axis'
| },
| legend: {
| data: [${legendData}]
| },
| grid: {
| left: '3%',
| right: '4%',
| bottom: '3%',
| containLabel: true
| },
| toolbox: {
| feature: {
| saveAsImage: {}
| }
| },
| xAxis: {
| type: 'category',
| boundaryGap: false,
| data: [${xAxisdata}]
| },
| yAxis: {
| type: 'value'
| },
| series: [
| ${series}
| ]
| };
| if (option && typeof option === 'object') {
| myChart.setOption(option);
| }
| </script>
| </body>
|</html>
|
|""".stripMargin
//存起来
IoUtil.writeUtf8(new FileOutputStream("./111.html"), true, html)
}

private def top = {
val res = map
.values
.flatten
.groupBy(_.getUser_id)
.map(m => {
(m._1, m._2.toList.sortBy(_.getTime))
})
.map(m => {
val allAuthorList: List[Author] = m._2

val now = LocalDate.now()

val day1List = allAuthorList.map(a => (LocalDateTime.parse(a.getTime, dateFormat).format(yyyyMMdd), a))
.groupBy(_._1)
.map(t => (t._1, t._2.map(_._2)))
.toList
.sortBy(_._1.toInt)

//这里计算包含当天,不是今天计算昨天的
val day_30 = day1List.filter(
ta =>
ta._1.toInt > now.minusDays(30).format(yyyyMMdd).toInt
&& ta._1.toInt <= now.format(yyyyMMdd).toInt
).flatMap(_._2)
val day_7 = day1List.filter(
ta =>
ta._1.toInt > now.minusDays(7).format(yyyyMMdd).toInt
&& ta._1.toInt <= now.format(yyyyMMdd).toInt
).flatMap(_._2)
val day_3 = day1List.filter(
ta =>
ta._1.toInt > now.minusDays(3).format(yyyyMMdd).toInt
&& ta._1.toInt <= now.format(yyyyMMdd).toInt
).flatMap(_._2)
val day_1 = day1List.filter(
ta => ta._1.toInt == now.format(yyyyMMdd).toInt
).flatMap(_._2)

def report(authorList: List[Author]): (Int, Int, Int, String, Int, String, Boolean, String) = {
if (authorList.isEmpty) {
return (0, 0, 0, "", 0, "", false, "")
}

//总数
val day_got_digg_count = authorList.last.getGot_digg_count.toInt - authorList.head.getGot_digg_count.toInt
val day_got_view_count = authorList.last.getGot_view_count.toInt - authorList.head.getGot_view_count.toInt

//单时间段最高
var max_got_digg_count = 0;
var max_got_digg_count_time = ""
var max_got_view_count = 0
var max_got_view_count_time = ""
val authorListSliding = authorList.sliding(2, 2)
authorListSliding.foreach(l => {
val head = l.head
val last = l.last

val digg = last.getGot_digg_count.toInt - head.getGot_digg_count.toInt
if (digg > max_got_digg_count) {
max_got_digg_count = digg
max_got_digg_count_time = s"${getOutTime(head.getTime)} - ${getOutTime(last.getTime)}"
}

val view = last.getGot_view_count.toInt - head.getGot_view_count.toInt
if (view > max_got_view_count) {
max_got_view_count = view
max_got_view_count_time = s"${getOutTime(head.getTime)} - ${getOutTime(last.getTime)}"
}
})

//有无升级
val authors = authorList.sortBy(_.getLevel)
var level = false
var levelDesc = "无升级"
val headLevel = authors.head.getLevel.toInt
val lastLevel = authors.last.getLevel.toInt
if ((lastLevel - headLevel) != 0) {
level = true
levelDesc = s"${headLevel} 升到 ${lastLevel}"
}
(day_got_digg_count, day_got_view_count, max_got_digg_count, max_got_digg_count_time, max_got_view_count, max_got_view_count_time, level, levelDesc)
}

val (day_30_total_got_digg_count, day_30_total_got_view_count, day_30_max_got_digg_count, day_30_max_got_digg_count_time, day_30_max_got_view_count, day_30_max_got_view_count_time, day_30_level, day_30_levelDesc) = report(day_30)
val (day_7_total_got_digg_count, day_7_total_got_view_count, day_7_max_got_digg_count, day_7_max_got_digg_count_time, day_7_max_got_view_count, day_7_max_got_view_count_time, day_7_level, day_7_levelDesc) = report(day_7)
val (day_3_total_got_digg_count, day_3_total_got_view_count, day_3_max_got_digg_count, day_3_max_got_digg_count_time, day_3_max_got_view_count, day_3_max_got_view_count_time, day_3_level, day_3_levelDesc) = report(day_3)
val (day_1_total_got_digg_count, day_1_total_got_view_count, day_1_max_got_digg_count, day_1_max_got_digg_count_time, day_1_max_got_view_count, day_1_max_got_view_count_time, day_1_level, day_1_levelDesc) = report(day_1)

val head = allAuthorList.head
(m._1, Map(
"user_name" -> head.getUser_name,
"user_id" -> head.getUser_id,
"day_30_total_got_digg_count" -> day_30_total_got_digg_count,
"day_30_total_got_view_count" -> day_30_total_got_view_count,
"day_30_max_got_digg_count" -> day_30_max_got_digg_count,
"day_30_max_got_digg_count_time" -> day_30_max_got_digg_count_time,
"day_30_max_got_view_count" -> day_30_max_got_view_count,
"day_30_max_got_view_count_time" -> day_30_max_got_view_count_time,
"day_30_level" -> day_30_level,
"day_30_levelDesc" -> day_30_levelDesc,

"day_7_total_got_digg_count" -> day_7_total_got_digg_count,
"day_7_total_got_view_count" -> day_7_total_got_view_count,
"day_7_max_got_digg_count" -> day_7_max_got_digg_count,
"day_7_max_got_digg_count_time" -> day_7_max_got_digg_count_time,
"day_7_max_got_view_count" -> day_7_max_got_view_count,
"day_7_max_got_view_count_time" -> day_7_max_got_view_count_time,
"day_7_level" -> day_7_level,
"day_7_levelDesc" -> day_7_levelDesc,

"day_3_total_got_digg_count" -> day_3_total_got_digg_count,
"day_3_total_got_view_count" -> day_3_total_got_view_count,
"day_3_max_got_digg_count" -> day_3_max_got_digg_count,
"day_3_max_got_digg_count_time" -> day_3_max_got_digg_count_time,
"day_3_max_got_view_count" -> day_3_max_got_view_count,
"day_3_max_got_view_count_time" -> day_3_max_got_view_count_time,
"day_3_level" -> day_3_level,
"day_3_levelDesc" -> day_3_levelDesc,

"day_1_total_got_digg_count" -> day_1_total_got_digg_count,
"day_1_total_got_view_count" -> day_1_total_got_view_count,
"day_1_max_got_digg_count" -> day_1_max_got_digg_count,
"day_1_max_got_digg_count_time" -> day_1_max_got_digg_count_time,
"day_1_max_got_view_count" -> day_1_max_got_view_count,
"day_1_max_got_view_count_time" -> day_1_max_got_view_count_time,
"day_1_level" -> day_1_level,
"day_1_levelDesc" -> day_1_levelDesc,

))
})
val list = res.values.toList

println("\n-----------------今日获赞Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总获赞")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_1_total_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_1_total_got_digg_count"))
})
println("\n-----------------近3日获赞Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总获赞")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_3_total_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_3_total_got_digg_count"))
})
println("\n-----------------近7日获赞Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总获赞")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_7_total_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_7_total_got_digg_count"))
})
println("\n-----------------近30日获赞Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总获赞")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_30_total_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_30_total_got_digg_count"))
})

println("\n-----------------今日浏览Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总浏览")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_1_total_got_view_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_1_total_got_view_count"))
})
println("\n-----------------近3日浏览Top10------------------")
printf("|%-12s\t|%-5s|\n", "用户", "总浏览")
printf("|%-12s\t|%-5s|\n", "-" * 12, "-" * 5)
list.sortBy(value => value("day_3_total_got_view_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_3_total_got_view_count"))
})

println("\n-----------------今日单时间段获赞Top10------------------")
printf("|%-12s\t|%-25s\t|%-5s|\n", "用户", "时间段", "获赞")
printf("|%-12s\t|%-25s\t|%-5s|\n", "-" * 12, "-" * 25, "-" * 5)
list.sortBy(value => value("day_1_max_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-25s\t|%-5s|\n", value("user_name"), value("day_1_max_got_digg_count_time"), value("day_1_max_got_digg_count"))
})

println("\n-----------------今日单时间段浏览Top10------------------")
printf("|%-12s\t|%-25s\t|%-5s|\n", "用户", "时间段", "获浏览")
printf("|%-12s\t|%-25s\t|%-5s|\n", "-" * 12, "-" * 25, "-" * 5)
list.sortBy(value => value("day_3_max_got_digg_count").asInstanceOf[Int])(Ordering.Int.reverse).take(10).foreach(value => {
printf("|%-12s\t|%-25s\t|%-5s|\n", value("user_name"), value("day_3_max_got_digg_count_time"), value("day_3_max_got_digg_count"))
})

println("\n-----------------今日升级名单------------------")
printf("|%-12s\t|%-5s|\n", "用户", "等级")
printf("|%-12s\t|%-10s|\n", "-" * 12, "-" * 10)
list.filter(value => value("day_1_level").asInstanceOf[Boolean]).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_1_levelDesc"))
})
println("\n-----------------近3日升级名单------------------")
printf("|%-12s\t|%-5s|\n", "用户", "等级")
printf("|%-12s\t|%-10s|\n", "-" * 12, "-" * 10)
list.filter(value => value("day_3_level").asInstanceOf[Boolean]).foreach(value => {
printf("|%-12s\t|%-5s|\n", value("user_name"), value("day_3_levelDesc"))
})
}

def load(): Unit = {
List(
"./j-20210701.json",
"./j-20210702.json",
"./j-20210703.json",
).foreach(path => {
val lineList = new util.ArrayList[String]()
IoUtil.readLines(new FileInputStream(path), StandardCharsets.UTF_8, lineList)
lineList.forEach(line => {
val type1: Type = new TypeReference[util.Map[String, util.List[Author]]] {}.getType
val bean: util.Map[String, util.List[Author]] = JSONUtil.toBean(line, type1, true)
bean.asScala.foreach(entry => map.put(entry._1, entry._2.asScala.toList))
})
})
}

def getOutTime(time: String): String = {
LocalDateTime.parse(time, dateFormat).format(dateFormatOut)
}

}

本文转载自: 掘金

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

java并发编程之FutureTask源码

发表于 2021-07-03

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

前言

说到FutureTask就不得不说到Callabl和Future;其中Callabl是一个接口,用来定义任务,且有返回值的地方,且可以有返回值。Future是用来获取Callabl执行结果的。本篇笔记主要写FutureTask源码的。

正文

1
2
3
4
java复制代码
public class FutureTask<V> implements RunnableFuture<V>{}

public interface RunnableFuture<V> extends Runnable, Future<V> {}

FutureTask实现了RunnableFuture接口。而RunnableFuture接口又继承了 Runnable接口和Future接口。

所以,FutureTask可以作为Runnable参数,传入Thread,然后启动线程执行任务。

常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
/** The underlying callable; nulled out after running */
private Callable<V> callable; // 被执行的任务

/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes //输出结果


/** The thread running the callable; CASed during run() */
private volatile Thread runner; // 执行任务的线程


/** Treiber stack of waiting threads */
private volatile WaitNode waiters; // 等待节点,用于存储等待结果的线程,链表结构

在FutureTask中定义了一个state,标记任务的执行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
/*
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0; // 新建
private static final int COMPLETING = 1; // 执行中
private static final int NORMAL = 2; // 正常执行完成
private static final int EXCEPTIONAL = 3; // 执行报错
private static final int CANCELLED = 4; // 取消
private static final int INTERRUPTING = 5; // 中断中
private static final int INTERRUPTED = 6; // 中断

其中状态之间的转换也在注释中已经说明。

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}

public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

构造方法有两个,都是将状态置为NEW;
其中接收Runnable参数的构造方法需要传入固定值,且使用Executors.callable方法将Runnable和result转为callable,该方法中用到了适配器模式。感兴趣的可以进源码去看看。

run方法

run方法是线程直接调用的地方。其实最根本的原理和Runnable的执行是一样的,只是这里多进行了一次包装,对执行结果进行了处理。

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
java复制代码
public void run() {
// 判断状态
if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

run方法看起来还是比较简单,就是执行call方法,然后保存执行结果。

这里主要看两个方法,setException和set。

setException

这个方法是执行任务出错后调用的方法,其中将错误信息作为参数。下面来看看源码:

1
2
3
4
5
6
7
8
java复制代码
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}

源码也比较简单:

  1. CAS将state从NEW转为COMPLETING
  2. 将错误信息赋予outcome
  3. 将state转为EXCEPTIONAL
  4. 执行finishCompletion方法

set方法

这个方法是执行任务成功后调用的方法,将执行结果作为参数传入该方法。源码如下:

1
2
3
4
5
6
7
8
java复制代码
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

流程和setException方法是一样的,唯一的区别就是第三步是将state转换为NORMAL。

finishCompletion

这个方法是在set和setException中都调用了的。注释说明的是:移除所有等待线程,并向线程发送信号,执行done()方法,置callable为null;

下面看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
private void finishCompletion() {
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) { // 循环存储线程的链表
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { // CAS修改waiters的值为null
for (;;) {
Thread t = q.thread; // 获取线程
if (t != null) {
q.thread = null; // 置为null
LockSupport.unpark(t); // 通知线程
}
WaitNode next = q.next; // 指向下一个节点
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}

源码也比较简单,就是for循环遍历等待线程的链表,然后通过LockSupport.unpark来通知线程。这里主要注意LockSupport.unpark的使用和原理。

其实线程执行任务的大概流程就是:

  1. 线程执行任务
  2. 判断执行结果,并保存结果(执行失败还是成功)
  3. 修改状态和保存结果信息
  4. 挨个通知等待中的线程(LockSupport.unpark)

isCancelled

根据状态判断是否被取消掉

1
2
3
4
java复制代码
public boolean isCancelled() {
return state >= CANCELLED;
}

isDone

判断线程是否完成

1
2
3
4
java复制代码
public boolean isDone() {
return state != NEW;
}

cancel

取消任务

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
public boolean cancel(boolean mayInterruptIfRunning) {
// CAS修改状态,根据mayInterruptIfRunning判断修改为INTERRUPTING还是CANCELLED
// 修改失败就返回false,修改成功则进行下一步
if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
// 是否要取消正在执行中的任务
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
t.interrupt(); //使用
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}

源码也比较简单,主要还是看mayInterruptIfRunning参数,这个参数就是是否取消正在执行中的任务。为true的话就使用Thread的interrupt来取消任务的执行。最后通知等待线程取消等待。

get

这个名字有两个方法,一个是一直等待,一个是等待有限时长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
// 这个方法是一直处于等待中,直到有结果为止
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING) // 如果状态在运行中或NEW,则等待(调用awaitDone)
s = awaitDone(false, 0L);
return report(s);
}
// 这个是等待有限时长
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}

大概流程就是:

  1. 判断状态,是否调用awaitDone
  2. 根据状态返回结果

两个方法都差不多,都是调用awaitDone方法,只是参数不一样。

awaitDone

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
java复制代码
private int awaitDone(boolean timed, long nanos)throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}

int s = state;
// 任务执行完成,返回状态码
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null) //构建节点
q = new WaitNode();
else if (!queued) // CAS修改waiters(头插法)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) { // 有限时间则使用该方法
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else //无限制时间
LockSupport.park(this);
}
}

这个方法主要就三点:

  1. 用当前线程构建WaitNode,然后使用CAS将WaitNode加入waiters中,waiters就是一个链表
  2. 使用LockSupport来阻塞线程(有限时长或无限时长)
  3. 成功则返回状态码

report

这个方法也比较简单

1
2
3
4
5
6
7
8
9
java复制代码
private V report(int s) throws ExecutionException {
Object x = outcome; //获取结果
if (s == NORMAL)
return (V)x; // 正常完成则将结果转换
if (s >= CANCELLED)
throw new CancellationException(); // 取消返回
throw new ExecutionException((Throwable)x); // 执行错误返回
}

最后

总结

整体来说,源代码是比较简单的。思想上来说,FutureTask就是一个中间类,实质可以看成是一个Runnable,其内部对Callable进行了包装(重写run方法),然后使用LockSupport来对线程阻塞和通知线程,使其达到等待任务执行完成,且获取结果的效果。

FutureTask中的state(状态),从执行任务的角度(callable)来看,严格意义上来说状态并没有完全一一对应,比如NEW状态,有可能任务已经完成,或者执行出错(只是结果还未保存到共享变量(outcome)中),这一点可以在run方法中看出,状态的修改是在Callable的call方法执行过后。如果FutureTask整体的角度来看就不一样了。

Callable的启动有两种方式:

第一种:将Callable作为参数,构建FutureTask,然后将FutureTask作为参数,创建Thread实例,然后启动start。

第二种:直接将Callable放入线程池的submit方法中,然后返回FutureTask。线程池的原理和第一种方式是一样的,线程池是在submit方法中实现的第一种的步骤。

本文转载自: 掘金

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

【微软算法面试高频题】回文对 1 题目 2 解析

发表于 2021-07-03

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播

  1. 题目

给定一组互不相同的单词, 找出所有不同的索引对 (i, j),使得列表中的两个单词, words[i] + words[j] ,可拼接成回文串。

1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码示例 1:
输入:words = ["abcd","dcba","lls","s","sssll"]
输出:[[0,1],[1,0],[3,2],[2,4]]
解释:可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]

示例 2:
输入:words = ["bat","tab","cat"]
输出:[[0,1],[1,0]]
解释:可拼接成的回文串为 ["battab","tabbat"]

示例 3:
输入:words = ["a",""]
输出:[[0,1],[1,0]]
  1. 解析

本题可以使用暴力做法,即美剧每一对字符串的组合,暴力判断他们是否能够构成回文串,时间复杂度O(n2∗m)O(n^2*m)O(n2∗m),其中nnn是字符串数量,mmm是字符串平均长度。但是很显然,这样的时间复杂度在实际工程中效率很低。

假设存在两个字符串s1s_1s1​和s2s_2s2​,s1+s2s_1+s_2s1​+s2​是一个回文串,其中两个字符串的长度分别为len1len_1len1​和len2len_2len2​: 1)如果len1=len2len_1=len_2len1​=len2​, 那么s1s_1s1​是s2s_2s2​的翻转。 2)如果len1>len2len_1>len_2len1​>len2​,这种情况下可以将s1s_1s1​拆分成两部分t1t_1t1​和t2t_2t2​,其中t1t_1t1​是s2s_2s2​的翻转,t2t_2t2​是一个回文串 3)如果len1<len2len_1 < len_2len1​<len2​, 这种情况下将s2s_2s2​拆分成两部分t1t_1t1​和t2t_2t2​, 其中t2t_2t2​是s1s_1s1​的翻转,t1t_1t1​是一个回文串

1.png

所以,当面对两个字符串,可以将两个字符串中较长的那一个拿出来,拆分成两部分,t1t_1t1​和t2t_2t2​:

  • 当t1t_1t1​是回文串的时候,符合情况3),只需要去检验t2t_2t2​字符串是否包含翻转
  • 当t2t_2t2​是回文串的时候,符合情况2),只需要去检验t1t_1t1​字符串是否包含翻转

所以,这就相当于要对每一个字符串都查询是否包含有回文串,然后将剩余的部分翻转并和其他字符串相匹配。对此,有两种实现方法:

  • 我们可以使用字典树存储所有的字符串。在进行查询时,我们将待查询串的子串逆序地在字典树上进行遍历,即可判断其是否存在。
  • 我们可以使用哈希表存储所有字符串的翻转串。在进行查询时,我们判断带查询串的子串是否在哈希表中出现,就等价于判断了其翻转是否存在。

其中字典树又称单词查找树、前缀树,是一种树形结构,利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,比哈希表更快。根节点不包含字符,除根节点外每个节点都只包含一个字符;从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;每个节点的所有子节点包含的字符都不相同。

一个简单的示例如下: 1)构建字典树

image.png

2)查找字符串”a”

image.png

所以,思路如下:

  • 构建
    字符串取反;
    遍历word,创建节点;
    word.substring(j+1)为回文,则添加到该节点的suffixs列表中;这里要注意word本身为回文则添加到root节点的suffixs列表中;
  • 查找
    遍历word;
    如果word.substring(j)为回文,则要看当前节点是否为一个单词,如果是,添加到结果中;对应情况3)
    word遍历结束且有以word结尾的单词,则要看当前节点的suffixs列表;对应情况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
63
64
65
66
67
68
69
70
71
72
73
74
75
c++复制代码class Solution {
public:
struct node {
int ch[26];
int flag;
node() {
flag = -1;
memset(ch, 0, sizeof(ch));
}
};

vector<node> tree;

void insert(string& s, int id) {
int len = s.length(), add = 0;
for (int i = 0; i < len; i++) {
int x = s[i] - 'a';
if (!tree[add].ch[x]) {
tree.emplace_back();
tree[add].ch[x] = tree.size() - 1;
}
add = tree[add].ch[x];
}
tree[add].flag = id;
}

int findWord(string& s, int left, int right) {
int add = 0;
for (int i = right; i >= left; i--) {
int x = s[i] - 'a';
if (!tree[add].ch[x]) {
return -1;
}
add = tree[add].ch[x];
}
return tree[add].flag;
}

bool isPalindrome(string& s, int left, int right) {
int len = right - left + 1;
for (int i = 0; i < len / 2; i++) {
if (s[left + i] != s[right - i]) {
return false;
}
}
return true;
}

vector<vector<int>> palindromePairs(vector<string>& words) {
tree.emplace_back(node());
int n = words.size();
for (int i = 0; i < n; i++) {
insert(words[i], i);
}
vector<vector<int>> ret;
for (int i = 0; i < n; i++) {
int m = words[i].size();
for (int j = 0; j <= m; j++) {
if (isPalindrome(words[i], j, m - 1)) {
int left_id = findWord(words[i], 0, j - 1);
if (left_id != -1 && left_id != i) {
ret.push_back({i, left_id});
}
}
if (j && isPalindrome(words[i], 0, j - 1)) {
int right_id = findWord(words[i], j, m - 1);
if (right_id != -1 && right_id != i) {
ret.push_back({right_id, i});
}
}
}
}
return ret;
}
};

哈希表

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
c++复制代码class Solution {
private:
vector<string> wordsRev;
unordered_map<string_view, int> indices;

public:
int findWord(const string_view& s, int left, int right) {
auto iter = indices.find(s.substr(left, right - left + 1));
return iter == indices.end() ? -1 : iter->second;
}

bool isPalindrome(const string_view& s, int left, int right) {
int len = right - left + 1;
for (int i = 0; i < len / 2; i++) {
if (s[left + i] != s[right - i]) {
return false;
}
}
return true;
}

vector<vector<int>> palindromePairs(vector<string>& words) {
int n = words.size();
for (const string& word: words) {
wordsRev.push_back(word);
reverse(wordsRev.back().begin(), wordsRev.back().end());
}
for (int i = 0; i < n; ++i) {
indices.emplace(wordsRev[i], i);
}

vector<vector<int>> ret;
for (int i = 0; i < n; i++) {
int m = words[i].size();
if (!m) {
continue;
}
string_view wordView(words[i]);
for (int j = 0; j <= m; j++) {
if (isPalindrome(wordView, j, m - 1)) {
int left_id = findWord(wordView, 0, j - 1);
if (left_id != -1 && left_id != i) {
ret.push_back({i, left_id});
}
}
if (j && isPalindrome(wordView, 0, j - 1)) {
int right_id = findWord(wordView, j, m - 1);
if (right_id != -1 && right_id != i) {
ret.push_back({right_id, i});
}
}
}
}
return ret;
}
};
  • 时间复杂度:O(n∗m2)O(n*m^2)O(n∗m2),其中nnn是字符串的数量,mmm是字符串的平均长度。对于每一个字符串,我们需要O(m2)O(m^2)O(m2)地判断其所有前缀与后缀是否是回文串,并O(m2)O(m^2)O(m2)地寻找其所有前缀与后缀是否在给定的字符串序列中出现。
  • 空间复杂度:O(n×m)O(n×m)O(n×m),其中 n 是字符串的数量,m 是字符串的平均长度为字典树的空间开销。

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播

本文转载自: 掘金

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

【2022年6月10日】掘金专栏100强排名

发表于 2021-07-02

【定期更新,建议收藏】
👉 最新更新时间:2022年06月10日16:00:00

本次参与排名的专栏数6442个,作者3070名。

为了发现掘金优秀作者,让同学们更好找到需要的专栏,我做了个专栏榜单。(经过数据比对,自从有了这个榜单,一些优秀专栏的订阅人数明显上升,它们的价值正在被更多的读者发现…)

参与本次榜单计算的作者来自于掘金 🏆推荐作者榜 的推荐、后端、前端、Android、IOS、人工智能、开发工具、代码人生和阅读等9类榜单,以及出现在首页推荐的文章作者,汇总后按照订阅数降序排名。

两点说明:

  • 如果你的专栏很优秀,但未入榜,这只是因为你暂时没有上作者榜而已,请在评论里留言,我会手动添加后参与排行;
  • 手机文字版排版影响阅读的话,可以往下滑动查看高清图片版。
排名 专栏名称 分类 订阅数 文章数 作者
1 学习源码整体架构系列 前端 3177 29 若川
2 2021最新前端面试题汇总 前端 1559 14 CUGGZ
3 Vue源码解析 前端 1246 11 Sunshine_Lin
4 Flutter 入门与实战 前端 1077 159 岛上码农
5 React进阶 前端 1007 21 我不是外星人
6 前端工程化 前端 999 6 CookieBoty
7 逛逛GitHub 综合 926 94 逛逛GitHub
8 Flutter House 前端 796 109 恋猫de小郭
9 精通 Vue 技术栈的源码原理 前端 770 20 李永宁
10 王者并发课 服务端🔥 724 30 秦二爷
11 Vue2源码系列 前端 620 12 Big shark@LX
12 前端开发真好玩呀 前端 607 25 大帅老猿
13 对线面试官 面试 534 39 Java3y
14 前端编译与工程化闭环 前端 520 38 zxg_神说要有光
15 超深度解读MySQL 服务端🔥 452 16 谦虚的小K
16 前端项目负责人培养 前端 446 19 耳东蜗牛
17 重学JavaScript系列 前端 416 24 CUGGZ
18 天天造轮子 前端 407 13 全栈然叔
19 低代码可视化 前端 404 13 徐小夕
20 程序员十三 服务端🔥 396 63 程序员十三
21 前端面试集合 前端 394 8 Big shark@LX
22 Vite 从入门到精通 前端 377 6 易师傅
23 前端 DevOps 前端 360 9 CookieBoty
24 刷穿 LeetCode 面试 333 546 宫水三叶的刷题日记
25 硬核JS 前端 331 7 isboyjc
26 重学安卓 客户端 294 6 KunMinX
27 2022 技术进阶计划 前端 280 8 春哥的梦想是摸鱼
28 字节教育前端 前端 279 92 ELab
29 前端面试知识点 前端 268 2 子弈
30 常见手写题系列 前端 268 6 前端胖头鱼
31 前端面试 前端 259 17 前端胖头鱼
32 Flutter基础 前端 253 100 张风捷特烈
33 前端工程化 前端 250 8 yck
34 前端进阶 前端 249 29 yck
35 快速入门Vue3 前端 246 28 LBJ
36 从零学 Go 服务端🔥 246 31 aoho
37 FlutterUnit 周边 前端 243 20 张风捷特烈
38 前端面试官 前端 234 2 伊人a
39 学透CSS 前端 234 69 前端picker
40 敖丙的絮絮叨叨 服务端🔥 230 22 敖丙
41 数据可视化 前端 228 22 Fly
42 前端面试指南 前端 220 5 yck
43 JavaScript 前端 219 27 前端胖头鱼
44 JavaGuide 服务端🔥 216 28 JavaGuide
45 组件库从0到1 前端 216 48 DevUI团队
46 JS【掘金安东尼】 前端 214 83 掘金安东尼
47 平凡的DDD 服务端🔥 211 5 秦二爷
48 跟煎鱼精通 Go 语言 服务端🔥 211 62 煎鱼eddycjy
49 TypeScript 系列 前端 209 16 冴羽
50 vue转react系列 前端 202 6 饼干_
51 SpringCloudAlibaba 服务端🔥 202 9 XiaoLin_Java
52 蹲坑也能进大厂– 并发编程从基础到成神 服务端🔥 200 16 JavaGieGie
53 JavaScript 深入系列 前端 200 18 冴羽
54 前端DevOps 前端 198 13 杨成功
55 服务端开发从入门到精通 服务端🔥 197 150 王中阳Go
56 前端百题斩 前端 196 39 执鸢者
57 若川视野 x 源码共读 前端 194 34 若川
58 Jetpack Compose 发射! 前端 193 25 fundroid
59 前端构建工具系列 前端 191 20 微医前端团队
60 Go语言学习专栏 服务端🔥 191 92 王中阳Go
61 React 面试必知必会 前端 184 13 洛竹
62 前端精读周刊 前端 184 214 黄子毅
63 精通 Webpack 核心原理 前端 177 14 Tecvan
64 一周面试一个大厂 面试 172 3 Sunshine_Lin
65 java毕设项目 服务端🔥 170 87 java毕设李杨勇
66 why技术 服务端🔥 170 108 why技术
67 Android开发者 客户端 169 118 Android_开发者
68 不止于面试【掘金安东尼】 面试 167 55 掘金安东尼
69 优质の思维导图 前端 164 11 LBJ
70 Vue3那些事 前端 162 17 杨村长
71 go-zero 服务端🔥 160 99 万俊峰Kevin
72 Redis系列 服务端🔥 158 7 捡田螺的小男孩
73 Linux实战 服务端🔥 157 59 杰哥的IT之旅
74 算法题每日一练 面试 157 115 知心宝贝
75 重修JavaScript 前端 156 4 前端阿飞
76 掘金 · 日新计划 掘金 152 12 掘金酱
77 Flutter核心渲染机制 前端 151 6 Nayuta
78 面试题解 面试 149 19 一尾流莺
79 把书读薄 - 设计模式之美 服务端🔥 147 26 coder-pig
80 Linux基础 服务端🔥 145 60 杰哥的IT之旅
81 技术专题写作 前端 145 27 掘金酱
82 Flutter 绘制探索 前端 145 14 张风捷特烈
83 前端进阶必看 前端 144 8 Big shark@LX
84 Go语言从入门到精通 服务端🔥 142 20 xcbeyond
85 重拾CSS的乐趣 前端 142 9 手撕红黑树
86 某科学的three.js 前端 138 12 alphardex
87 一起精读 Vue 官方文档吧! 前端 137 52 FULL_STOP
88 Electron+Vue3 MAC 版日历开发记录 前端 136 34 叶梅树
89 字节跳动移动平台团队-iOS 客户端 132 9 字节跳动终端技术
90 3D 前端 132 11 dragonir
91 年中-年终总结 130 12 掘金酱
92 Java面试精选 面试 130 52 Java中文社群
93 ES6 系列 前端 130 20 冴羽
94 青训营笔记 掘金 130 23 YK菌
95 Flink 0-1 电商实时数仓项目 服务端🔥 128 7 摸鱼专家
96 前端基础 前端 128 4 小浪努力学前端
97 Java 面经手册 服务端🔥 127 16 小傅哥
98 GitHub 开源项目 综合 126 23 杰哥的IT之旅
99 🥉【JVM技术体系】技术研究院 服务端🔥 126 60 李浩宇Alex
100 GitHub 热点速递 综合 126 51 HelloGitHub

图片悦览版

image.png


关注公众号【MetaThoughts】,可以及时获取专栏更新通知。

本文转载自: 掘金

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

《springcloud超级入门》Spring Boot简介

发表于 2021-07-02

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式进行配置,从而使开发人员不再需要定义样板化的配置。

Spring Boot 致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

在使用 Spring Boot 之前,我们需要搭建一个项目框架并配置各种第三方库的依赖,还需要在 XML 中配置很多内容。

Spring Boot 完全打破了我们之前的使用习惯,一分钟就可以创建一个 Web 开发的项目;通过 Starter 的方式轻松集成第三方的框架;去掉了 XML 的配置,全部用注解代替。

Spring Boot Starter 是用来简化 jar 包依赖的,集成一个框架只需要引入一个 Starter,然后在属性文件中配置一些值,整个集成的过程就结束了。

不得不说,Spring Boot 在内部做了很多的处理,让开发人员使用起来更加简单了。

总结了一些使用 Spring Boot 开发的优点:

  • 基于 Spring 开发 Web 应用更加容易。
  • 采用基于注解方式的配置,避免了编写大量重复的 XML 配置。
  • 可以轻松集成 Spring 家族的其他框架,比如 Spring JDBC、Spring Data 等。
  • 提供嵌入式服务器,令开发和部署都变得非常方便。

本文转载自: 掘金

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

GoLang 配置代理:系统代理 & GOPROXY

发表于 2021-07-02

问题描述

由于一些网络环境问题,当我们使用go get 来获取一些依赖包时,常常会出现 connection refused 等问题,下面获取gomega 包的指令为例:

1
shell复制代码go get -u github.com/onsi/gomega

经过等待后,我们会得到如下报错:

1
bash复制代码go get: module github.com/onsi/gomega: Get "https://proxy.golang.org/github.com/onsi/gomega/@v/list": dial tcp 172.217.27.145:443: connect: connection refused

解决方案

设置GOPROXY 环境变量

go 安装后,默认的环境变量,我们可以通过go env 指令查看一下:

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
bash复制代码GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/root/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/root/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.16.5"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build525051245=/tmp/go-build -gno-record-gcc-switches"

其中,可以看到一个GOPROXY 环境变量,正常情况下,go get 会通过该代理访问github 以完成包的下载。
如何使用呢,值得庆幸的是goproxy.io 这个开源项目为我们提供了一个免费的代理,其官网提供了不同平台的配置方式,以linux 为例,这里提供两种方式进行配置tongguo:

  1. 通过export 设置环境变量
1
shell复制代码export GOPROXY=https://goproxy.io,direct

但是熟悉linux 的同学知道,通过export 设置的环境变量,当我们重新登陆到终端时就会失效,为了一劳永逸的解决问题,建议使用长久生效的方式设置该环境变量
2. 通过go env 设置环境变量

1
shell复制代码go env -w GOPROXY=https://goproxy.io,direct

通过这种方式,可以将环境变量写入go 的配置文件中,长久生效

设置系统代理

说到网络问题,那么很自然的就会联想到四个大字——设置代理,上一个方法中,我们是通过为go 自身配置代理来解决网络访问问题的。

我们也可以通过设置系统代理的方式,来解决该问题。

1
2
shell复制代码export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port

最终效果

通过配置代理,解决go get 无法为项目下载依赖的问题。

1
2
3
4
5
6
bash复制代码# go get -u github.com/onsi/gomega
go: downloading github.com/onsi/gomega v1.13.0
go: downloading golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading golang.org/x/net v0.0.0-20210614182718-04defd469f4e
go: downloading golang.org/x/text v0.3.6

本文转载自: 掘金

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

五秒向MySQL插入十万的数据! 👨‍🏫 10w+数据5S

发表于 2021-07-02

👨‍🏫 10w+数据5S 打向MySQL.

其实这里也是可以横向扩展到100W+数据,只是把goroutine调大一点即可.

💡 1. 简单实现思路:

当我们使用一个 goroutine 的去进行增加的时候,不难会发现,会增加的很慢,我当初在用 Java 尝试使用一个 Conn 增加了1w 个数据(那个时候是测试上万数据查询效率问题),那个过程可想而知,等了我好久。。。

这几天,在写需求的时候,需要一次性(一次HTTP请求)打向 MySQL 中,我首先是通过事务来写入的,但是发现真的好慢,所以我改成了如下模式,通过在当前线程分发指定的数据给一个 goroutine,让该协程负责打向MySQL指定数量的数据。但是这个度要把握好,什么意思呢?总价下来就是两个问题:1. 一共开多少 goroutine 2. 每个 goroutine负责多少数据量呢? 这里我的依据是一个 goroutine负责 1000条数据,那么根据当前需要插入多少条数据就可以计算出一共需要多少个 goroutine了. 代码如下.

💡 2. 代码.

我把生成MD5的xcommon和日志组件xlogging删掉了,需要的可以自行补全.

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

import (
"errors"
"strconv"
"time"
)

// CDKItem 表示一个CDK: t_cd_key_store.
// 对应数据库中的表.
// 表明随意,我主要是为了切合场景.
type CDKItem struct {
Id uint64 `json:"id" xorm:"id"`
BillNo string `json:"bill_no" xorm:"bill_no"`
Md5CdKey string `json:"md5_cd_key" xorm:"md5_cd_key"`
ExchangeActID string `json:"exchange_act_id" xorm:"exchange_act_id"`
StatusUsed uint64 `json:"status_used" xorm:"status_used"`
CreateTime string `json:"create_time" xorm:"create_time"`
LastModifier string `json:"last_modifier" xorm:"last_modifier"`
ModTime string `json:"mod_time" xorm:"mod_time"`
}

const (
CDKItemTable = "t_cd_key_store"
OneExecSQL = 1000
)

// BatchInsertCDK 将生成的所有数据进行返回.
func (c *CDKItem) BatchInsertCDK(data map[int64]string, log *xlogging.Entry) ([][]string, error) {

log.Info("BatchInsertCDK start.. Parameter len(map): ", len(data))
if len(data) <= 0 {
return nil,errors.New("cdk 数量为0")
}
start := time.Now().Unix()
rows := make([][]string, len(data))
index := 0
datas := make(map[int64]string, 10000)
// error同步.
ch := make(chan int, 5)
// 记录1w中的下标.
i := 0
// 开的协程数.
nums := 0

for cdkNum, cdk := range data {

datas[cdkNum] = cdk
num := strconv.Itoa(int(cdkNum))
row := []string{num, xcommon.GetMD5(cdk)}
rows[index] = row
index++
i++
if i == OneExecSQL {
go batch(datas, ch, log)
log.Info("开启第[" + strconv.Itoa(nums) + "]个goroutine")
datas = make(map[int64]string, OneExecSQL)
i = 0
nums++
continue
}

if len(data)-(nums*OneExecSQL) > 0 && len(data)-(nums*OneExecSQL) < OneExecSQL {
go batch(datas, ch, log)
log.Info("开启第[" + strconv.Itoa(nums) + "]个goroutine")
nums++
datas = make(map[int64]string)
}

}

count := 0
for {
err := <-ch
count++
if err == 1 {
// 异常.
return nil, errors.New("batch insert error")
}
if count == nums {
break
}
}

end := time.Now().Unix()
log.Info("向MySQL中插入数据耗费时间: ", end-start)
return rows, nil
}

func batch(data map[int64]string, ch chan int, log *xlogging.Entry) {

session := db.NewSession()
defer session.Close()
session.Begin()
start := time.Now().Unix()
for cdkNum, cdk := range data {
item := &CDKItem{
BillNo: strconv.Itoa(int(cdkNum)),
Md5CdKey: xcommon.GetMD5(cdk),
CreateTime: xcommon.GetNowTime(),
ModTime: xcommon.GetNowTime(),
}
_, err := session.Table(CDKItemTable).Insert(item)
if nil != err {
session.Rollback()
log.Info("batch insert cdk item error session.Rollback: ", err.Error())
ch <- 1
return
}
}
err := session.Commit()
if err != nil {
log.Info("batch insert cdk item error session.Commit: ", err.Error())
ch <- 1
return
}
// 正常.
ch <- 0
end := time.Now().Unix()
log.Info("batch goroutine 执行耗时: ", end-start)
}

效果图:

image-20210702161919521

  • StartTime

image-20210702161959635

  • EndTime

image-20210702161941555

修改Batch方法,

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复制代码func batch(data map[int64]string, ch chan int, actExchangeID string, log *xlogging.Entry) {

buf := make([]byte, 0, len(data))
buf = append(buf, "insert into t_cd_key_store(bill_no,md5_cd_key,exchange_act_id,create_time,mod_time) values"...)
index := 0
t := time.Now().Format("2006-01-02 15:04:05")
for cdkNum, cdk := range data {

cdk = xcommon.GetMD5(cdk)
noi := strconv.Itoa(int(cdkNum))
index++
if index == len(data) {
buf = append(buf, "(\""+noi+"\",\""+cdk+"\",\""+actExchangeID+"\",\""+t+"\",\""+t+"\");"...)
} else {
buf = append(buf, "(\""+noi+"\",\""+cdk+"\",\""+actExchangeID+"\",\""+t+"\",\""+t+"\"),"...)
}

}

_, err := db.Exec(string(buf))
if nil != err {
log.Info("batch insert cdk item error : ", err.Error())
ch <- 1
return
}
ch <- 0
}

如果改用这样的SQL来进行增加,时间可以压缩在5秒之内.

💡 3. 总结:

其实以后在面对这种大数据量增加的情况下,我们只需要协调 goroutine 的数量以及每个 goroutine 负责的数据部分即可。只要调理好了,10S百万数据也不是不可以的哈~

如果要搞10S百万数据的话,我们可以分10个goroutine去处理,每个分10w条数据,那么当10个 goroutine同时进行的时候,就相当于一个goroutine在进行,所以把时间进行压缩、CPU利用率提高就可以达成10S百万数据的存储了。

本文转载自: 掘金

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

盘点 AOP AOP 代理类的创建

发表于 2021-07-02

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

一 .前言

上一篇聊过了 AOP 的初始化 , 了解了 AOP 是如何通过 PostProcess 完成 AOP 代理类的创建 ,

二 . AOP 创建起点

AOP 创建的

1
2
3
4
JAVA复制代码// 在 AbstractAutoProxyCreator # wrapIfNecessary 中 , 发起了 createProxy 流程用于创建代理类
createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean))

下面来看一下该逻辑中主要做了什么 :

2.1 AbstractAutoProxyCreator 创建 Proxy 代理类

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
java复制代码C- AbstractAutoProxyCreator
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

// 构建一个 ProxyFactory
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
} else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

// 构建通知者
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

// 获取代理对象 -> 2.3 Aop 对象创建详情
// org.springframework.aop.framework.ProxyFactory
return proxyFactory.getProxy(getProxyClassLoader());
}

[PIC] : specificInterceptors 对象内容

aop_interceptor_list.jpg

[Pro] AutoProxyUtils 的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码C- AutoProxyUtils : 自动代理的工具类
M- shouldProxyTargetClass : 确定是否应该用目标类而不是接口代理给定bean
M- determineTargetClass : 确定指定bean的原始目标类,否则返回到常规的getType查找
M- exposeTargetClass : 公开指定bean的给定目标类
M- isOriginalInstance : 根据 ORIGINAL_INSTANCE_SUFFIX 确定给定bean名是否指示“原始实例”

static void exposeTargetClass(
ConfigurableListableBeanFactory beanFactory, @Nullable String beanName, Class<?> targetClass) {

if (beanName != null && beanFactory.containsBeanDefinition(beanName)) {
// ORIGINAL_TARGET_CLASS_ATTRIBUTE => org.springframework.aop.framework.autoproxy.AutoProxyUtils.originalTargetClass
beanFactory.getMergedBeanDefinition(beanName).setAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE, targetClass);
}
}

// PS : 这里设置了 Attribute 属性 , 后面专门看一下该属性会在什么情况下使用 >>>

[Pro] ProxyFactory 的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
用于编程使用的AOP代理的工厂 , 该类提供了在定制用户代码中获取和配置AOP代理实例的简单方法
C- ProxyFactory
MC- ProxyFactory() : 创建一个 ProxyFactory
MC- ProxyFactory(Object target) : 创建时通过 target 设置 Target 和 Interfaces 属性
MC- ProxyFactory(Class<?>... proxyInterfaces) : 没有目标,只有接口且必须添加拦截器
MC- ProxyFactory(Class<?> proxyInterface, Interceptor interceptor) : 用于为单个拦截器创建代理的便利方法
MC- ProxyFactory(Class<?> proxyInterface, TargetSource targetSource) :为指定的TargetSource创建一个ProxyFactory,使代理实现指定的接口
M- getProxy() : 根据这个工厂的设置创建一个新的代理 , 可反复调用
M- getProxy(@Nullable ClassLoader classLoader) :
M- getProxy(Class<T> proxyInterface, Interceptor interceptor)
M- getProxy(Class<T> proxyInterface, TargetSource targetSource)
M- getProxy(TargetSource targetSource)

P- proxyInterface :
P- Interceptor :
P- TargetSource :

[Pro] Advisor 作用

1
2
3
4
5
6
7
java复制代码保存AOP通知(在连接点上要采取的动作)的基本接口和决定通知(如切入点)适用性的过滤器 , Advisor接口允许支持不同类型的通知,比如 Before Advice 和 After Advice

C- Advisor : 通知器
M- getAdvice()
M- isPerInstance()

// 这里说一说 Advisor 是什么时候调用的 >>>

Advisor-system.png

2.2 buildAdvisors 创建通知对象

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
java复制代码C- AbstractAutoProxyCreator
protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) {
// 2-2-1 : 将指定的拦截器名称解析为Advisor对象
Advisor[] commonInterceptors = resolveInterceptorNames();

List<Object> allInterceptors = new ArrayList<>();
if (specificInterceptors != null) {
allInterceptors.addAll(Arrays.asList(specificInterceptors));
if (commonInterceptors.length > 0) {
if (this.applyCommonInterceptorsFirst) {
allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
} else {
allInterceptors.addAll(Arrays.asList(commonInterceptors));
}
}
}

Advisor[] advisors = new Advisor[allInterceptors.size()];
for (int i = 0; i < allInterceptors.size(); i++) {
// 2-2-2 : 将指定的拦截器名称解析为Advisor对象
advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
}
return advisors;
}


// 2-2-1 : 将指定的拦截器名称解析为Advisor对象
private Advisor[] resolveInterceptorNames() {
BeanFactory bf = this.beanFactory;
ConfigurableBeanFactory cbf = (bf instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory) bf : null);
List<Advisor> advisors = new ArrayList<>();
// 获取通用 interceptor 对象 , 该对象可以 setInterceptorNames 设置
for (String beanName : this.interceptorNames) {
if (cbf == null || !cbf.isCurrentlyInCreation(beanName)) {
Object next = bf.getBean(beanName);
advisors.add(this.advisorAdapterRegistry.wrap(next));
}
}
return advisors.toArray(new Advisor[0]);
}


// 2-2-2 : 将指定的拦截器名称解析为Advisor对象
C- DefaultAdvisorAdapterRegistry
public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
if (adviceObject instanceof Advisor) {
return (Advisor) adviceObject;
}
if (!(adviceObject instanceof Advice)) {
throw new UnknownAdviceTypeException(adviceObject);
}
Advice advice = (Advice) adviceObject;
if (advice instanceof MethodInterceptor) {
// So well-known it doesn't even need an adapter.
return new DefaultPointcutAdvisor(advice);
}
for (AdvisorAdapter adapter : this.adapters) {
// Check that it is supported.
if (adapter.supportsAdvice(advice)) {
return new DefaultPointcutAdvisor(advice);
}
}
throw new UnknownAdviceTypeException(advice);
}

三 . Aop 代理类的创建流程

上一节通过 CreateProxy 发起了代理的创建 , 此节来说一下 ProxyFactory 的创建流程

Step 1 : 获取代理类主流程 => ProxyFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 类结构 ===================
C- ProxyFactory
E- ProxyCreatorSupport

public Object getProxy(@Nullable ClassLoader classLoader) {
// createAopProxy() => org.springframework.aop.framework.ObjenesisCglibAopProxy
return createAopProxy().getProxy(classLoader);
}

// ProxyCreatorSupport 是 ProxyFactory 的父类 , createAopProxy 会调用该类方法
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
// 激活代理配置
activate();
}
// getAopProxyFactory() => org.springframework.aop.framework.DefaultAopProxyFactory
return getAopProxyFactory().createAopProxy(this);
}

Step 2 : AOP 代理工厂的选择

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
java复制代码// Aop 基于 Proxy 代理 , 主要接口为 AopProxy , 而 AopProxy 的创建接口类为 AopProxyFactory , 
public interface AopProxyFactory {

/**
* 为给定的AOP配置创建一个AopProxy
*/
AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;

}

// Aop 代理工厂默认为 DefaultAopProxyFactory
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// config.isOptimize() : 返回代理是否应该执行主动优化
// config.isProxyTargetClass() : 返回是否直接代理目标类以及任何接口
// hasNoUserSuppliedProxyInterfaces(config) : 确定所提供的AdvisedSupport是否只指定了SpringProxy接口(或者根本没有指定代理接口)
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
} else {
return new JdkDynamicAopProxy(config);
}
}


// PS : Spring 类默认会使用 JdkDynamicAopProxy , 自行定制的 AOP 类通常使用 CglibAopProxy

可以通过以下几种方式配置 :


// 方式一 : 构建 Bean 时可以通过配置切换 , 例如 :
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
// 设置代理目标对象
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}

// 方式二 : 配置 AOP 代理方式
1. 通过 EnableAspectJAutoProxy 配置
2. 通过属性 spring.aop.proxy-target-class 进行配置

Step 3-1 : CglibAopProxy 创建代理类

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
java复制代码
C- CglibAopProxy
public Object getProxy(@Nullable ClassLoader classLoader) {

try {
Class<?> rootClass = this.advised.getTargetClass();

Class<?> proxySuperClass = rootClass;
// String CGLIB_CLASS_SEPARATOR = "$$";
// 包含 $$
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}

// 检查是否已经验证了所提供的Class,如果没有,则验证它
validateClassIfNecessary(proxySuperClass, classLoader);

// 配置 CGLIB 增强 -> RPO31001
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// 生成代理类并创建代理实例
return createProxyClassAndInstance(enhancer, callbacks);
}
catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}


protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
enhancer.setInterceptDuringConstruction(false);
enhancer.setCallbacks(callbacks);
// 通过是否存在构造参数分别创建
return (this.constructorArgs != null && this.constructorArgTypes != null ?
enhancer.create(this.constructorArgTypes, this.constructorArgs) :
enhancer.create());
}

// RPO31001 补充 : Enhancer 的作用

通常我们常见的代理方式是通过 Proxy 类 , 而 Enhancer 也是一个方法代理类 , Proxy是基于接口的方式进行代理,Enhancer是基于继承的方式代理

Step 3-2 : JdkDynamicAopProxy

1
2
3
4
5
6
7
java复制代码// JdkDynamicAopProxy 构建代理类
public Object getProxy(@Nullable ClassLoader classLoader) {
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 创建代理类
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

AopProxy 代理体系

System-AopProxy.png

1
2
3
java复制代码Spring 中的代理类通常有 2个类 : CglibAopProxy / JdkDynamicAopProxy

// [Pro] : 如何切换 AopProxy 代理类型

四 . 要点深入

4.1 ORIGINAL_TARGET_CLASS_ATTRIBUTE 属性的使用

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
java复制代码// Step 1 : AbstractApplicationContext # refresh
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
try {
//.................

// 实例化所有剩余的(非lazy-init)单例
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}
//........
}
}

// PS : 可以看到 , 在主流程倒数第二步中 ,

// EventListenerMethodProcessor : 对标注了 @EventListener 的方法进行解析, 然后转换为一个 ApplicationListener
// Step 2: C- EventListenerMethodProcessor # afterSingletonsInstantiated
public void afterSingletonsInstantiated() {
ConfigurableListableBeanFactory beanFactory = this.beanFactory;
String[] beanNames = beanFactory.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
if (!ScopedProxyUtils.isScopedTarget(beanName)) {
Class<?> type = null;
try {
// 确定指定bean的原始目标类
// Step 2-1 : 获取 originalTargetClass
// beanName -> org.springframework.context.annotation.internalConfigurationAnnotationProcessor
// type -> org.springframework.context.annotation.ConfigurationClassPostProcessor
type = AutoProxyUtils.determineTargetClass(beanFactory, beanName);
}
catch (Throwable ex) {
//...............
}
if (type != null) {
if (ScopedObject.class.isAssignableFrom(type)) {
try {

Class<?> targetClass = AutoProxyUtils.determineTargetClass(
beanFactory, ScopedProxyUtils.getTargetBeanName(beanName));
if (targetClass != null) {
type = targetClass;
}
}
catch (Throwable ex) {
//...............
}
}
try {
// Step 2-3 : 获取 originalTargetClass
processBean(beanName, type);
}
catch (Throwable ex) {
//...............
}
}
}
}
}

// Step 2-1 : 从 BeanDefinition 中获取属性 originalTargetClass
String ORIGINAL_TARGET_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass");
BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName);
Class<?> targetClass = (Class<?>) bd.getAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE);

// Step 2-2 : 查找原始目标类 , 可以看到 , 通常查询的类都是 internalXXX 开头的类 , 该类为 beanDefinitionNames 中创建
public static Class<?> determineTargetClass(
ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) {

if (beanName == null) {
return null;
}
if (beanFactory.containsBeanDefinition(beanName)) {
BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName);
Class<?> targetClass = (Class<?>) bd.getAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE);
if (targetClass != null) {
return targetClass;
}
}
return beanFactory.getType(beanName);
}


// Step 2-3 : 该方法省略 , 主要是 EventListener 注解的处理 , 和主流程无关
private void processBean(final String beanName, final Class<?> targetType)

4.2 AOP Cglib 配置流程

通常基础的AOP 代理是通过

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
java复制代码org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration

public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

// h
AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
//
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}


public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
// AUTO_PROXY_CREATOR_BEAN_NAME => org.springframework.aop.config.internalAutoProxyCreator
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
}
}

补充一 : EnableAspectJAutoProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public @interface EnableAspectJAutoProxy {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is {@code false}.
*/
boolean proxyTargetClass() default false;

/**
* Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
* for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
* Off by default, i.e. no guarantees that {@code AopContext} access will work.
* @since 4.3.1
*/
boolean exposeProxy() default false;

}

补充二 : AopAutoConfiguration 自动配置类

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
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {

}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class ClassProxyingConfiguration {

ClassProxyingConfiguration(BeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
}

}

}

补充三 : org.springframework.aop.config.internalAutoProxyCreator 的作用

1
2
java复制代码C- AopConfigUtils # String AUTO_PROXY_CREATOR_BEAN_NAME = "org.springframework.aop.config.internalAutoProxyCreator"
?- 内部管理的自动代理创建器的bean名

总结

讲道理 , 这篇文章其实没写好 , 很多地方现在都没弄清楚 , 精力有限无法在细致深入 , 总得来说算是一个半成品吧 , 后面时间充裕了 , 再来深入看一下

核心概念 :

  • AbstractAutoProxyCreator # createProxy 发起代理类的创建
  • AutoProxyUtils 为工具类 , 用于原类和代理的一些常见操作处理
  • 通过 AopProxyFactory 创建代理类 , 有2种 : ObjenesisCglibAopProxy / JdkDynamicAopProxy

本文转载自: 掘金

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

并发场景下的死锁原因及规避解决方法

发表于 2021-07-02

一、处理解决死锁的思路

处理思路: 预防 → 避免 → 检测 → 解除。

如果能预防是最好, 其次是尽可能的避免,最后如果检测到死锁, 那就想办法尽快的解除它。

二、产生死锁的四个要素

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。 但实际场景中, 我们不能破坏互斥性,要解决死锁问题,就需要破坏其他三个条件。

三、 死锁的检测算法:

file

这里有三个进程四个资源,每个数据代表的含义如下:

  • E 向量:资源总量
  • A 向量:资源剩余量
  • C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
  • R 矩阵:每个进程请求的资源数量

进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。

死锁检测算法总结如下:

每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。

  1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。
  2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
  3. 如果没有这样一个进程,算法终止。

四、 MySQL的死锁问题原因

  1. InnoDB 隔离级别、索引和锁的关系
    假设一张消息表(msg),里面有3个字段。id是主键,token是非唯一索引,message没有索引。
    Innodb对于主键使用了聚簇索引,这是一种数据存储方式,表数据是和主键一起存储,主键索引的叶结点存储行数据。对于普通索引,其叶子节点存储的是主键值。
    file
  2. 索引与锁的关系

1)ID操作, SQL语句: delete from msg where id=2;
由于根据ID删除, 产生行锁, 锁住单条记录。
file

2)索引操作,SQL语句:delete from msg where token=’cvs’;
由于token是二级索引, 会锁住二级索引所对应的记录,共两行。
file

3. 全表扫描,SQL语句: delete from msg where message=‘订单号’;
![file](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/68b53b53b0371f672ab153679646d953b87617af03cb6c795c9f24c1fb5a9b5b)
  1. 幻读与间隙锁关系
    事务A在第一次查询时得到1条记录,在第二次执行相同查询时却得到两条记录。从事务A角度上看是见鬼了! 其实这就是幻读,尽管加了行锁,但还是避免不了幻读。
    file
    InnoDB默认隔离级别是可重复读(Repeatable read(RR)), 所谓可重复读是指: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    如何解决幻读,就需要采用gap间隙锁。

比如事务A查询完数据后, 执行update更新: update msg set message=‘订单’ where token=‘asd’;
因为token是二级索引, 除了在索引的记录上添加X锁,此外,还在’asd’与相邻两个索引的区间加上锁。
file
当事务B在新增记录, 执行insert into msg values (null,‘asd’,’hello’); commit;时,会首先检查这个区间是否被锁上,此时asd已经被锁上,则不能立即执行,需要等待该gap锁被释放。这样就能避免幻读问题。

五、 如何尽量规避死锁

在实际的应用场景中, 不能百分百的避免死锁, 只能遵循规范尽量的减少死锁的产生:

  • 采用固定的顺序去访问表和行数据。比如两个job批量更新的场景,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;另外,将两个事务的sql顺序调整为一致,也能避免死锁。
  • 将大事务拆分成小事务。大事务更倾向于死锁,如果业务允许,尽量将大事务拆成小事务, 减少一个事务大批量的数据更新操作。
  • 在同一个事务操作中,尽可能做到一次锁定所需要的所有资源,不要被其他事物抢占,减少死锁概率。
  • 尽可能的走主键ID或索引进行更新。可以看到如果不走索引或ID,将会进行全表扫描,会对每一行记录添加上锁,很容易造成死锁。
  • 降低事物的隔离级别。此方法影响面比较广,如果业务允许,将隔离级别调低也是较好的选择,比如将魔力隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。

本文由mirson创作分享,如需进一步交流,请加QQ群:19310171或访问www.softart.cn

本文转载自: 掘金

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

1…623624625…956

开发者博客

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