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

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


  • 首页

  • 归档

  • 搜索

Linux命令拾遗-剖析工具

发表于 2021-11-08

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

简介

这是Linux命令拾遗系列的第五篇,本篇主要介绍Linux中常用的线程与内存剖析工具,以及更高级的perf性能分析工具等。

本系列文章索引

Linux命令拾遗-入门篇

Linux命令拾遗-文本处理篇

Linux命令拾遗-软件资源观测

Linux命令拾遗-硬件资源观测

像vmstat,top等资源观测命令,只能观测到资源整体使用情况,以及在各进程/线程上的使用情况,但没法了解到进程/线程为什么会使用这么多资源?

这种情况下,可以通过剖析(profile)工具做进一步分析,如jstack,jmap,pstack,gcore,perf等。

jstack

jstack是JVM下的线程剖析工具,可以打印出当前时刻各线程的调用栈,这样就可以了解到jvm中各线程都在干什么了,如下:

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
bash复制代码$ jstack 3051
2021-11-07 21:55:06
Full thread dump OpenJDK 64-Bit Server VM (11.0.12+7 mixed mode):

Threads class SMR info:
_java_thread_list=0x00007f3380001f00, length=10, elements={
0x00007f33cc027800, 0x00007f33cc22f800, 0x00007f33cc233800, 0x00007f33cc24b800,
0x00007f33cc24d800, 0x00007f33cc24f800, 0x00007f33cc251800, 0x00007f33cc253800,
0x00007f33cc303000, 0x00007f3380001000
}

"main" 1 prio=5 os_prio=0 cpu=2188.06ms elapsed=1240974.04s tid=0x00007f33cc027800 nid=0xbec runnable [0x00007f33d1b68000]
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(java.base@11.0.12/Native Method)
at java.io.FileInputStream.read(java.base@11.0.12/FileInputStream.java:279)
at java.io.BufferedInputStream.read1(java.base@11.0.12/BufferedInputStream.java:290)
at java.io.BufferedInputStream.read(java.base@11.0.12/BufferedInputStream.java:351)
- locked <0x00000007423a5ba8> (a java.io.BufferedInputStream)
at sun.nio.cs.StreamDecoder.readBytes(java.base@11.0.12/StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(java.base@11.0.12/StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(java.base@11.0.12/StreamDecoder.java:178)
- locked <0x0000000745ad1cf0> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(java.base@11.0.12/InputStreamReader.java:181)
at java.io.Reader.read(java.base@11.0.12/Reader.java:189)
at java.util.Scanner.readInput(java.base@11.0.12/Scanner.java:882)
at java.util.Scanner.findWithinHorizon(java.base@11.0.12/Scanner.java:1796)
at java.util.Scanner.nextLine(java.base@11.0.12/Scanner.java:1649)
at com.example.clientserver.ClientServerApplication.getDemo(ClientServerApplication.java:57)
at com.example.clientserver.ClientServerApplication.run(ClientServerApplication.java:40)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:804)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:788)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:333)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298)
at com.example.clientserver.ClientServerApplication.main(ClientServerApplication.java:27)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(java.base@11.0.12/Native Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(java.base@11.0.12/NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(java.base@11.0.12/DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(java.base@11.0.12/Method.java:566)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)

"Reference Handler" 2 daemon prio=10 os_prio=0 cpu=2.76ms elapsed=1240973.97s tid=0x00007f33cc22f800 nid=0xbf3 waiting on condition [0x00007f33a820a000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.12/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@11.0.12/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.12/Reference.java:213)

实例:找占用CPU较高问题代码

如果你发现你的java进程CPU占用一直都很高,可以用如下方法找到问题代码:

1,找出占用cpu的线程号pid

1
2
bash复制代码# -H表示看线程,其中312是java进程的pid
$ top -H -p 312

2,转换线程号为16进制

1
2
3
bash复制代码# 其中62是从top中获取的高cpu的线程pid
$ printf "%x" 314
13a

3,获取进程中所有线程栈,提取对应高cpu线程栈

1
bash复制代码$ jstack 312 | awk -v RS= '/0x13a/'

通过这种方法,可以很容易找到类似大循环或死循环之类性能极差的代码。

实例:查看各线程数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码$ jstack 10235|grep -oP '"[^"]+"'|sed -E 's/[0-9]+/n/g'|sort|uniq -c|sort -nr
8 "GC Thread#n"
2 "Gn Conc#n"
2 "Cn CompilerThreadn"
1 "main"
1 "VM Thread"
1 "VM Periodic Task Thread"
1 "Sweeper thread"
1 "Signal Dispatcher"
1 "Service Thread"
1 "Reference Handler"
1 "Gn Young RemSet Sampling"
1 "Gn Refine#n"
1 "Gn Main Marker"
1 "Finalizer"
1 "Common-Cleaner"
1 "Attach Listener"

如上,通过uniq等统计jvm各线程数量,可用于快速确认业务线程池中线程数量是否正常。

jmap

jmap是JVM下的内存剖析工具,用来分析或导出JVM堆数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码# 查看对象分布直方图,其中3051是java进程的pid
$ jmap -histo:live 3051 | head -n20
num instances bytes class name (module)
-------------------------------------------------------
1: 19462 1184576 [B (java.base@11.0.12)
2: 3955 469920 java.lang.Class (java.base@11.0.12)
3: 18032 432768 java.lang.String (java.base@11.0.12)
4: 11672 373504 java.util.concurrent.ConcurrentHashMap$Node (java.base@11.0.12)
5: 3131 198592 [Ljava.lang.Object; (java.base@11.0.12)
6: 5708 182656 java.util.HashMap$Node (java.base@11.0.12)
7: 1606 155728 [I (java.base@11.0.12)
8: 160 133376 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@11.0.12)
9: 1041 106328 [Ljava.util.HashMap$Node; (java.base@11.0.12)
10: 6216 99456 java.lang.Object (java.base@11.0.12)
11: 1477 70896 sun.util.locale.LocaleObjectCache$CacheEntry (java.base@11.0.12)
12: 1403 56120 java.util.LinkedHashMap$Entry (java.base@11.0.12)
13: 1322 52880 java.lang.ref.SoftReference (java.base@11.0.12)
14: 583 51304 java.lang.reflect.Method (java.base@11.0.12)
15: 999 47952 java.lang.invoke.MemberName (java.base@11.0.12)
16: 29 42624 [C (java.base@11.0.12)
17: 743 41608 java.util.LinkedHashMap (java.base@11.0.12)
18: 877 35080 java.lang.invoke.MethodType (java.base@11.0.12)

也可以直接将整个堆内存转储为文件,如下:

1
2
3
4
5
bash复制代码$ jmap -dump:format=b,file=heap.hprof 3051
Heap dump file created

$ ls *.hprof
heap.hprof

堆转储文件是二进制文件,没法直接查看,一般是配合mat(Memory Analysis Tool)等堆可视化工具来进行分析,如下:

mat打开hprof文件后,会看下如下一个概要界面。

mat

点击Histogram可以按类维度查询内存占用大小
histogram

点击Dominator Tree可以看到各对象总大小(Retained Heap,包含引用的子对象),以及所占内存比例,可以看到一个ArrayList对象占用99.31%,看来是个bug,找到创建ArrayList的代码,修改即可。

dominator_tree

可以看到,使用jmap+mat很容易分析内存泄露bug,但需要注意的是,jmap执行时会让jvm暂停,对于高并发的服务,最好先将问题节点摘除流量后再操作。

另外,可以在jvm上添加参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump/,使得在发生内存溢出时,自动生成堆转储文件到dump目录。

mat下载地址:www.eclipse.org/mat/

pstack

pstack是c/c++等原生程序的线程剖析工具,类似jstack,不过是面向原生程序的,用法如下:

1
bash复制代码$ pstack $pid

例如,mysql是用c/c++写的,当mysql运行hang住时,可以通过pstack来查看线程栈。

如下,执行一个update SQL卡住了,我们来分析分析为啥?

1,使用processlist找出问题SQL的线程id

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码mysql> select t.thread_id,t.thread_os_id,pl.* from information_schema.processlist pl 
join performance_schema.threads t on pl.id = t.processlist_id;
+-----------+--------------+----+------+---------------------+------+---------+------+-----------+---------------------------------------------------------------------------------------------------------->
| thread_id | thread_os_id | ID | USER | HOST | DB | COMMAND | TIME | STATE | INFO >
+-----------+--------------+----+------+---------------------+------+---------+------+-----------+---------------------------------------------------------------------------------------------------------->
| 32 | 4850 | 7 | root | 10.142.72.126:34934 | NULL | Query | 0 | executing | select t.thread_id,t.thread_os_id,pl.* from information_schema.processlist pl join performance_schema.thr>
| 33 | 5771 | 8 | root | 10.142.112.35:54795 | NULL | Sleep | 488 | | NULL >
| 34 | 4849 | 9 | root | 10.142.112.35:54796 | demo | Sleep | 442 | | NULL >
| 35 | 5185 | 10 | root | 10.142.112.35:54797 | demo | Query | 403 | updating | update order set status=1 where oid=1 >
| 37 | 28904 | 12 | root | 10.142.72.126:34936 | demo | Sleep | 411 | | NULL >
+-----------+--------------+----+------+---------------------+------+---------+------+-----------+---------------------------------------------------------------------------------------------------------->
5 rows in set (0.00 sec)

可以看到,处理update SQL的线程的thread_os_id是5185

2,使用pstack获取线程栈

1
2
bash复制代码# 其中3287是mysqld进程的pid
$ pstack 3287 > mysqld_stack.log

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
bash复制代码$ cat mysqld_stack.log | awk -v RS= '$1 ~ /5185/'
Thread 33 (Thread 0x7fe3c05ea700 (LWP 5185)):
0 futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x7fe358025474) at ../sysdeps/nptl/futex-internal.h:183
1 __pthread_cond_wait_common (abstime=0x0, clockid=0, mutex=0x7fe358025420, cond=0x7fe358025448) at pthread_cond_wait.c:508
2 __pthread_cond_wait (cond=0x7fe358025448, mutex=0x7fe358025420) at pthread_cond_wait.c:638
3 0x000055c1b85b55f8 in os_event::wait (this=0x7fe358025408) at /home/work/softwares/mysql-5.7.30/storage/innobase/os/os0event.cc:179
4 0x000055c1b85b4a24 in os_event::wait_low (this=0x7fe358025408, reset_sig_count=6) at /home/work/softwares/mysql-5.7.30/storage/innobase/os/os0event.cc:366
5 0x000055c1b85b51bc in os_event_wait_low (event=0x7fe358025408, reset_sig_count=0) at /home/work/softwares/mysql-5.7.30/storage/innobase/os/os0event.cc:611
6 0x000055c1b8579097 in lock_wait_suspend_thread (thr=0x7fe35800ab60) at /home/work/softwares/mysql-5.7.30/storage/innobase/lock/lock0wait.cc:315
7 0x000055c1b863f860 in row_mysql_handle_errors (new_err=0x7fe3c05e6cf4, trx=0x7fe41c3c18d0, thr=0x7fe35800ab60, savept=0x0) at /home/work/softwares/mysql-5.7.30/storage/innobase/row/row0mysql.cc:783
8 0x000055c1b868514b in row_search_mvcc (buf=0x7fe35801eab0 "\377", mode=PAGE_CUR_GE, prebuilt=0x7fe35800a3a0, match_mode=1, direction=0) at /home/work/softwares/mysql-5.7.30/storage/innobase/row/row0sel.cc:6170
9 0x000055c1b84d83e1 in ha_innobase::index_read (this=0x7fe35801e7c0, buf=0x7fe35801eab0 "\377", key_ptr=0x7fe35800c400 "\001", key_len=8, find_flag=HA_READ_KEY_EXACT) at /home/work/softwares/mysql-5.7.30/storage/innobase/handler/ha_innodb.cc:8755
10 0x000055c1b7a7c4fc in handler::index_read_map (this=0x7fe35801e7c0, buf=0x7fe35801eab0 "\377", key=0x7fe35800c400 "\001", keypart_map=1, find_flag=HA_READ_KEY_EXACT) at /home/work/softwares/mysql-5.7.30/sql/handler.h:2818
11 0x000055c1b7a6ca36 in handler::ha_index_read_map (this=0x7fe35801e7c0, buf=0x7fe35801eab0 "\377", key=0x7fe35800c400 "\001", keypart_map=1, find_flag=HA_READ_KEY_EXACT) at /home/work/softwares/mysql-5.7.30/sql/handler.cc:3046
12 0x000055c1b7a7774a in handler::read_range_first (this=0x7fe35801e7c0, start_key=0x7fe35801e8a8, end_key=0x7fe35801e8c8, eq_range_arg=true, sorted=true) at /home/work/softwares/mysql-5.7.30/sql/handler.cc:7411
13 0x000055c1b7a7556e in handler::multi_range_read_next (this=0x7fe35801e7c0, range_info=0x7fe3c05e7af8) at /home/work/softwares/mysql-5.7.30/sql/handler.cc:6476
14 0x000055c1b7a764c7 in DsMrr_impl::dsmrr_next (this=0x7fe35801ea20, range_info=0x7fe3c05e7af8) at /home/work/softwares/mysql-5.7.30/sql/handler.cc:6868
15 0x000055c1b84ebb84 in ha_innobase::multi_range_read_next (this=0x7fe35801e7c0, range_info=0x7fe3c05e7af8) at /home/work/softwares/mysql-5.7.30/storage/innobase/handler/ha_innodb.cc:20574
16 0x000055c1b830debe in QUICK_RANGE_SELECT::get_next (this=0x7fe358026ed0) at /home/work/softwares/mysql-5.7.30/sql/opt_range.cc:11247
17 0x000055c1b801a01c in rr_quick (info=0x7fe3c05e7d90) at /home/work/softwares/mysql-5.7.30/sql/records.cc:405
18 0x000055c1b81cbb1d in mysql_update (thd=0x7fe358000e10, fields=..., values=..., limit=18446744073709551615, handle_duplicates=DUP_ERROR, found_return=0x7fe3c05e8338, updated_return=0x7fe3c05e8340) at /home/work/softwares/mysql-5.7.30/sql/sql_update.cc:819
19 0x000055c1b81d262d in Sql_cmd_update::try_single_table_update (this=0x7fe3580090e0, thd=0x7fe358000e10, switch_to_multitable=0x7fe3c05e83d7) at /home/work/softwares/mysql-5.7.30/sql/sql_update.cc:2906
20 0x000055c1b81d2b9a in Sql_cmd_update::execute (this=0x7fe3580090e0, thd=0x7fe358000e10) at /home/work/softwares/mysql-5.7.30/sql/sql_update.cc:3037
21 0x000055c1b8108f4e in mysql_execute_command (thd=0x7fe358000e10, first_level=true) at /home/work/softwares/mysql-5.7.30/sql/sql_parse.cc:3616
22 0x000055c1b810ed9b in mysql_parse (thd=0x7fe358000e10, parser_state=0x7fe3c05e9530) at /home/work/softwares/mysql-5.7.30/sql/sql_parse.cc:5584
23 0x000055c1b8103afe in dispatch_command (thd=0x7fe358000e10, com_data=0x7fe3c05e9de0, command=COM_QUERY) at /home/work/softwares/mysql-5.7.30/sql/sql_parse.cc:1491
24 0x000055c1b81028b2 in do_command (thd=0x7fe358000e10) at /home/work/softwares/mysql-5.7.30/sql/sql_parse.cc:1032
25 0x000055c1b824a4bc in handle_connection (arg=0x55c1bdca11f0) at /home/work/softwares/mysql-5.7.30/sql/conn_handler/connection_handler_per_thread.cc:313
26 0x000055c1b8955119 in pfs_spawn_thread (arg=0x55c1bdb3fc20) at /home/work/softwares/mysql-5.7.30/storage/perfschema/pfs.cc:2197
27 0x00007fe4253f8609 in start_thread (arg=<optimized out>) at pthread_create.c:477
28 0x00007fe424fd3103 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

从调用栈不难了解到,ha_innobase::index_read应该是读取索引数据的函数,调用row_search_mvcc时,走到一个lock_wait_suspend_thread函数中去了,看起来SQL执行过程中好像由于等待什么锁,被阻塞住了。

接下来,通过如下语句获取一下锁等待信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码mysql> SELECT r.trx_mysql_thread_id waiting_thread,r.trx_query waiting_query,
-> concat(timestampdiff(SECOND,r.trx_wait_started,CURRENT_TIMESTAMP()),'s') AS duration,
-> b.trx_mysql_thread_id blocking_thread,t.processlist_command state,b.trx_query blocking_current_query,e.sql_text blocking_last_query
-> FROM information_schema.innodb_lock_waits w
-> JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
-> JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
-> JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id
-> JOIN performance_schema.events_statements_current e USING(thread_id);
+----------------+------------------------------------------+----------+-----------------+-------+------------------------+----------------------------------------------------+
| waiting_thread | waiting_query | duration | blocking_thread | state | blocking_current_query | blocking_last_query |
+----------------+------------------------------------------+----------+-----------------+-------+------------------------+----------------------------------------------------+
| 10 | update order set status=1 where oid=1 | 48s | 12 | Sleep | NULL | select * from order where oid=1 for update |
+----------------+------------------------------------------+----------+-----------------+-------+------------------------+----------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

喔,原来有个for update查询语句,对这条数据加了锁,导致这个update SQL阻塞了。

注:ubuntu中pstack包年久失修,基本无法使用,想在ubuntu中使用,可以将centos中的pstack命令复制过来

gcore

gcore是原生程序的内存转储工具,类似jmap,可以将程序的整个内存空间转储为文件,如下:

1
2
3
4
5
bash复制代码# 其中10235是进程的pid
$ gcore -o coredump 10235

$ ll coredump*
-rw-r--r-- 1 work work 3.7G 2021-11-07 23:05:46 coredump.10235

转储出来的dump文件,可以直接使用gdb进行调试,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码$ gdb java coredump.10235 
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from java...
(No debugging symbols found in java)
[New LWP 10235]
...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `java'.
#0 __pthread_clockjoin_ex (threadid=139794527459072, thread_return=0x7ffd83e54538, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>) at pthread_join_common.c:145
145 pthread_join_common.c: No such file or directory.
[Current thread is 1 (Thread 0x7f2474987040 (LWP 10235))]
(gdb)

另外,可以如下配置内核参数,使得程序发生内存访问越界/段错误时,自动生成coredump文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码# 开启coredump
$ ulimit -c unlimited
# 持久化方式
$ echo "ulimit -c unlimited" >> /etc/profile

# 设置coredump的命名规则
$ vi /etc/sysctl.conf
kernel.core_pattern=%e.core.%s_%t
# core_pattern占位变量解释
%p pid
%u uid
%g gid
%s signal number
%t UNIX time of dump
%h hostname
%e executable filename
# 查看coredump命名规则配置
$ cat /proc/sys/kernel/core_pattern

有时,在java进程上执行jmap时,会无法执行成功,这时可以使用gcore替代生成coredump,然后使用jmap转换为mat可以分析的hprof文件。

1
bash复制代码$ jmap -dump:format=b,file=heap.hprof `which java` coredump.10235

线程栈分析方法

一般来说,jstack配合top只能定位类似死循环这种非常严重的性能问题,由于cpu速度太快了,对于性能稍差但又不非常差的代码,单次jstack很难从线程栈中捕捉到问题代码。

因为性能稍差的代码可能只会比好代码多耗1ms的cpu时间,但这1ms就比我们的手速快多了,当执行jstack时线程早已执行到非问题代码了。

既然手动执行jstack不行,那就让脚本来,快速执行jstack多次,虽然问题代码对于人来说执行太快,但对于正常代码来说,它还是慢一些的,因此当我快速捕捉多次线程栈时,问题代码出现的次数肯定比正常代码出现的次数要多。

1
2
3
4
5
6
7
8
bash复制代码# 每0.2s执行一次jstack,并将线程栈数据保存到jstacklog.log
$ while sleep 0.2;do \
pid=$(pgrep -n java); \
[[ $pid ]] && jstack $pid; \
done > jstacklog.log

$ wc -l jstacklog.log
291121 jstacklog.log

抓了这么多线程栈,如何分析呢?我们可以使用linux中的文本命令来处理,按线程栈分组计数即可,如下:

1
2
3
4
5
6
bash复制代码$ cat jstacklog.log \
|sed -E -e 's/0x[0-9a-z]+/0x00/g' -e '/^"/ s/[0-9]+/n/g' \
|awk -v RS="" 'gsub(/\n/,"\\n",$0)||1' \
|sort|uniq -c|sort -nr \
|sed 's/$/\n/;s/\\n/\n/g' \
|less

jstack

出现次数最多的线程栈,大概率就是性能不佳或阻塞住的代码,上图中com.example.demo.web.controller.TestController.select方法栈抓取到2053次,是因为我一直在压测这一个接口,所以它被抓出来最多。

火焰图

可以发现,用文本命令分析线程栈并不直观,好在性能优化大师Brendan Gregg发明了火焰图,并开发了一套火焰图生成工具。

工具下载地址:github.com/brendangreg…

将jstack抓取的一批线程栈,生成一个火焰图,如下:

1
2
3
bash复制代码$ cat jstacklog.log \
| ./FlameGraph/stackcollapse-jstack.pl --no-include-tname \
| ./FlameGraph/flamegraph.pl --cp > jstacklog.svg

flamegraph

如上,生成的火焰图是svg文件,使用浏览器打开即可,在火焰图中,颜色并没有实际含义,它将相同的线程栈聚合在一起显示,因此,图中越宽的栈表示此栈在运行过程中,被抓取到的次数越多,也是我们需要重点优化的地方。

perf

perf是Linux官方维护的性能分析工具,它可以观测很多非常细非常硬核的指标,如IPC,cpu缓存命中率等,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码# ubuntu安装perf,包名和内核版本相关,可以直接输入perf命令会给出安装提示
sudo apt install linux-tools-5.4.0-74-generic linux-cloud-tools-5.4.0-74-generic

# cpu的上下文切换、cpu迁移、IPC、分支预测
sudo perf stat -a sleep 5
# cpu的IPC与缓存命中率
sudo perf stat -e cycles,instructions,cache-references,cache-misses,bus-cycles -a sleep 10
# cpu的1级数据缓存命中率
sudo perf stat -e L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores -a sleep 10
# 页表缓存TLB命中率
sudo perf stat -e dTLB-loads,dTLB-load-misses,dTLB-prefetch-misses -a sleep 10
# cpu的最后一级缓存命中率
sudo perf stat -e LLC-loads,LLC-load-misses,LLC-stores,LLC-prefetches -a sleep 10

# Count system calls by type for the specified PID, until Ctrl-C:
sudo perf stat -e 'syscalls:sys_enter_*' -p PID -I1000 2>&1 | awk '$2 != 0'
# Count system calls by type for the entire system, for 5 seconds:
sudo perf stat -e 'syscalls:sys_enter_*' -a sleep 5 2>&1| awk '$1 != 0'
# Count syscalls per-second system-wide:
sudo perf stat -e raw_syscalls:sys_enter -I 1000 -a

当然,它也可以用来抓取线程栈,并生成火焰图,如下:

1
2
3
4
5
6
7
8
9
bash复制代码# 抓取60s的线程栈,其中1892是mysql的进程pid
$ sudo perf record -F 99 -p 1892 -g sleep 60
[ perf record: Woken up 5 times to write data ]
[ perf record: Captured and wrote 1.509 MB perf.data (6938 samples) ]

# 生成火焰图
$ sudo perf script \
| ./FlameGraph/stackcollapse-perf.pl \
| ./FlameGraph/flamegraph.pl > mysqld_flamegraph.svg

mysql_flamegraph

如上所示,使用perf生成的mysql的火焰图,perf抓取线程栈相比jstack的优点是,抓取精度比jstack更高,运行开销比jstack更小,并且还可以抓到Linux内核调用栈!

当然,如此强大的它,也需要像root这样至高无上的权限才能使用。

注:perf抓取线程栈,是顺着cpu上的栈寄存器找到当前线程的调用栈的,因此它只能抓到当前正在cpu上运行线程的线程栈,因此通过perf可以非常容易做oncpu分析,分析high cpu问题。

注意:某些情况下,perf获取到的mysql线程栈是破碎的,类似_Z16dispatch_commandP3THDPK8COM_DATA19enum_server_command这种,非常难读,这种情况下,可以先使用sudo perf script | c++filt命令处理下c++函数命名问题,再将输出给到stackcollapse-perf.pl脚本,c++filt使用效果如下:

1
2
3
4
5
6
7
8
9
bash复制代码# 破碎的函数名
$ objdump -tT `which mysqld` | grep dispatch_command
00000000014efdf3 g F .text 000000000000252e _Z16dispatch_commandP3THDPK8COM_DATA19enum_server_command
00000000014efdf3 g DF .text 000000000000252e Base _Z16dispatch_commandP3THDPK8COM_DATA19enum_server_command

# 使用c++filt处理过后的函数名
$ objdump -tT `which mysqld` | grep dispatch_command | c++filt
00000000014efdf3 g F .text 000000000000252e dispatch_command(THD*, COM_DATA const*, enum_server_command)
00000000014efdf3 g DF .text 000000000000252e Base dispatch_command(THD*, COM_DATA const*, enum_server_command)

offcpu火焰图

线程在运行的过程中,要么在CPU上执行,要么被锁或io操作阻塞,从而离开CPU进去睡眠状态,待被解锁或io操作完成,线程会被唤醒而变成运行态。

如下:

thread_states

当线程在cpu上运行时,我们称其为oncpu,当线程阻塞而离开cpu后,称其为offcpu。

而线程离开CPU和之后被唤醒,就是我们常说的线程上下文切换,如果线程是因为io或锁等待而主动让出cpu,称为自愿上下文切换,如果是时间片用尽而让出cpu,称为非自愿上下文切换。

如果当线程在睡眠之前记录一下当前时间,然后被唤醒时记录当前线程栈与阻塞时间,再使用FlameGraph工具将线程栈绘制成一张火焰图,这样我们就得到了一张offcpu火焰图,火焰图宽的部分就是线程阻塞较多的点了,这就需要再介绍一下bcc工具了。

bcc

bcc是使用Linux ebpf机制实现的一套工具集,包含非常多的底层分析工具,如查看文件缓存命中率,tcp重传情况,mysql慢查询等等,如下:
bcc_tracing_tools

另外,它还可以做offcpu分析,生成offcpu火焰图,如下:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码# ubuntu安装bcc工具集
$ sudo apt install bpfcc-tools
# 使用root身份进入bash
$ sudo bash
# 获取jvm函数符号信息
$ ./FlameGraph/jmaps
# 抓取offcpu线程栈30s,83101是mysqld的进程pid
$ offcputime-bpfcc -df -p 83101 30 > offcpu_stack.out
# 生成offcpu火焰图
$ cat offcpu_stack.out \
| sed 's/;::/:::/g' \
| ./FlameGraph/flamegraph.pl --color=io --title="Off-CPU Time Flame Graph" --countname=us > offcpu_stack.svg

offcpu_flamegraph

如上图,我绘制了一张mysql的offcpu火焰图,可以发现大多数线程的offcpu都是由锁引起的,另外,offcpu火焰图与oncpu火焰图稍有不同,oncpu火焰图宽度代表线程栈出现次数,而offcpu火焰图宽度代表线程栈阻塞时间。

在我分析此图的过程中,发现JOIN::make_join_plan()函数最终竟然去调用了btr_estimate_n_rows_in_range_low这个函数,显然JOIN::make_join_plan()应该是用于制定SQL执行计划的,为啥好像去读磁盘数据了?经过在网上尝试搜索btr_estimate_n_rows_in_range_low,发现mysql有个机制,当使用非唯一索引查询数据时,mysql为了执行计划的准确性,会实际去访问一些索引数据,来辅助评估当前索引是否应该使用,避免索引统计信息不准导致的SQL执行计划不优。

我看了看相关表与SQL,发现当前使用的索引,由于之前研究分区表而弄成了非唯一索引了,实际上它可以设置为唯一索引的,于是我把索引调整成唯一索引,再次压测SQL,发现QPS从2300增加到了2700左右。

这里,也可以看出,对于一些没读过源码的中间件(如mysql),也是可以尝试使用perf或bcc生成火焰图来分析的,不知道各函数是干啥用的,可以直接把函数名拿到baidu、google去搜索,瞄一瞄别人源码解析的文章了解其作用。

bcc项目地址:github.com/iovisor/bcc

往期内容

Linux命令拾遗-入门篇

原来awk真是神器啊

Linux文本命令技巧(上)

Linux文本命令技巧(下)

字符编码解惑

本文转载自: 掘金

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

Mysql 温故知新系列「where 子查询」

发表于 2021-11-08

「这是我参与11月更文挑战的第 8 天,活动详情查看:2021最后一次更文挑战」

本章主要介绍基本的 where 子查询,学会如何使用基本的比较操作符,以及如何应对日常中最常见的查询需求

where 查询

上一篇文章介绍了基础的查询+排序,这一种是查出来全部数据。有时候我们需要用一些条件来对数据做过滤,这里需要使用关键字 where

在 where 的条件查询中,我们一般会使用到如下比较符:

  • >,匹配字段里大于某个值的全部记录
  • <,匹配小于某个值的记录
  • =,精准匹配值相等的记录,常见于 id 查询
  • <> 或 !=,判断为不等于某个值的记录
  • between..and.. 判断字段的值位于某个区间,可以拆分为两个条件组合判断,即 column>? and column<?
  • and 多个条件组合判断
  • or 多个条件或判断
  • in 判断字段的值是否位于某个集合中 这个集合通常会有多个候选值
  • is null 判断字段为空
  • is not null 判断值非空

需要 null 判断,是因为我们在建表的时候,有的字段允许为空,且未设计默认值,故此类型的字段则默认未 null

这里列举常见条件查询

根据指定字段查询

以我们最常使用的 id 字段为例

1
sql复制代码select * from rfid_info where id=?

因为 id 在表设计时通常作为主键,上述的操作会查询出唯一的一条记录。有时我们需要多个字段进行筛选,如下 sql

1
2
3
4
5
6
sql复制代码-- 1
select * from rfid_info where type=? and code>? or status=?
-- 2
select * from rfid_info where type=? and (code>? or status=?)
-- 3
select * from rfid_info where (type=? and code>?) or status=?

强烈建议, and, or 两种操作符尽可能的不要同一级进行组合判断,这样的操作可能会匹配不到我们希望的结果。最佳实践是,根据语义,用 () 吧相关的条件包裹起来,这样会减少不必要的失误

根据 id 集合查询多个

我们可以在 java 中查询出全部数据,再使用 Stream.filter() 过滤出满足 id 的数据,但非常不推荐

推荐使用 in 在数据库层面上直接处理好数据进行返回。在 sql 拼接时,需要吧 id 的集合转化为 (1, 2, ...) 的格式,用小括号包裹,用 , 隔开

1
sql复制代码select * from rfid_info where id in (1,2,3)

范围筛选

我们可以使用 >, < 来判断日期类型数据的大小(对于字符串类型,mysql 中使用 ascll 码进行大小的比较)

1
sql复制代码select * from rfid_info where time>? and name<?

对于选取满足区间条件的 sql,可以使用 between 关键字

1
2
3
sql复制代码select * from rfid_info where time between ? and ?
-- 等效于用 大于,小于两个组合条件进行判断
select * from rfid_info where time > ? and time < ?

本文转载自: 掘金

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

Redis 如何保证原子性来应对并发访问(八)

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

​ 在使用Redis时不可避免地会遇到并发访问的问题,比如多个用户同时下单,就会对缓存中的商品库存数据进行并发更新。一旦有了并发写操作,数据就会被修改,如果没有做好并发控制,就会导致数据被修改错误,影响到业务的正常使用。(例如秒杀场景下的超卖情况)

​ 为了保证并发访问的正确性,Redis提供了两个方法,分别是加锁和原子操作。

​ 原子操作是指执行过程中保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。这样既保证了并发控制,还能减少对系统并发性能的影响。

​ Redis加锁会有两个问题,一方面是加锁操作多,会降低系统的并发访问性能。另一方面Redis客户端加锁时,需要用到分布式锁,而这需要额外的存储系统来提供加解锁的操作。

原子操作

​ 并发控制针对的操作范围主要是数据修改操作。当有多个客户端对同一份数据执行RMW(Read-Modify-Write)操作时,我们就需要RMW操作涉及的代码以原子性方式执行。访问同一份数据的RMW操作代码,就叫做临界区代码。RMW操作即是指客户端要对数据做修改操作时所需要执行的步骤,即要先读取Redis中的内存数据到客户端中,然后在本地修改,最后写入到Redis服务中。而这部分操作就是指临界区的操作逻辑了。

Redis 的两种原子操作方法

​ 为了实现并发控制要求的临界区代码互斥执行。Redis的原子操作采用了两种方法:

  • 把多个操作在Redis中实现一个操作,也就是单命令操作;
  • 把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本。

单命令操作

​ 虽然Redis的单个命令可以原子性执行,但实际操作中数据修改包含了多个命令的操作,包括数据读取、数据增减、写回数据三个操作。

​ 这种情况就需要使用Redis提供的单命令操作了。例如INCR/DECR命令就可以实现数据的增减操作,而且因为它们本身就是单个命令操作,所以在执行它们时,就保证了它们的互斥性。

Lua脚本

​ 如果只是简单的增减操作,那么就可以使用单命令保证其原子性了。但是可能会有更复杂的判断逻辑或者其它操作,那么就需要通过封装多个命令在Lua脚本执行操作了。

​ Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。但是如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。所以在编写Lua脚本时需要避免把不需要做并发控制的操作写入脚本中。

分布式加锁

​ 在应对并发问题时,除了原子操作,Redis客户端可采用加锁的方法,来控制并发写操作对共享数据的修改,从而保证数据的正确性。

​ 当有多个客户端需要争抢锁时,需要保证的是这把锁不能是某个客户端本地的锁。否则的话,其它客户端的是无法访问到这把锁的,更不要说是获取锁了。所以在分布式系统中,当有多个客户端需要获取锁时,需要使用分布式锁。此时锁是保存在共享存储系统中的,可以被多个客户端共享访问和获取。

​ 而Redis正好可以被多个客户端共享访问,可以保存分布式锁。

加锁操作和释放锁的操作就是针对锁的键值进行读取、判断、设置的过程。

  • 加锁时根据锁变量值判断是否可以加锁,如果可以则对锁变量值进行修改,表示持有锁;
  • 释放锁时同样需要进行判断,因为需要判断当前加锁的是不是该客户端,如果不判断直接释放锁的话,会被其它客户端将持有的锁给释放掉了;如果可以释放锁,则重置锁变量的值;

这样一来,因为加锁释放锁涉及了多个操作,所以实现分布式锁时需要两个保证:

  • 锁操作的原子性;
  • 分布式锁的可靠性;

锁操作的原子性

锁操作的原子性可以采用上面提到的单命令操作和Lua脚本操作。

单命令操作和Lua脚本

使用 SETNX 和 DEL命令即可实现加锁和释放锁的操作。SETNX命令表示在执行时会判断键值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

SETNX key value

释放锁时直接将锁删除掉即可。

但进行操作时需要注意两个问题:

  • 一是锁的过期时间设置;在加锁后,如果后面的逻辑发生了异常导致没有释放锁,这时就需要过期时间去保证该客户端不能一直持有锁。
  • 还有一个是需要区别不同客户端的释放锁操作;这可以让每个客户端加锁时设置唯一值;

锁过期时间的设置和释放锁的操作都需要保证原子性;这里使用SET命令的NX选项 和Lua脚本保证了。

SET KEY VALUE [EX seconds | PX milliseconds] [NX]

即SET lock_key unique_value NX PX 10000 表示给lock_key这个键设置unique_value值,同时设置过期时间为10000ms。

释放锁也包含了读取锁变量值、判断锁变量值和删除锁变量三个操作,不过,我们无法使用单个命令来实现,所以,我们可以采用 Lua 脚本执行释放锁操作,通过 Redis 原子性地执行 Lua 脚本,来保证释放锁操作的原子性。

释放锁时的Lua脚本:

1
2
3
4
5
6
lua复制代码//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
1
lua复制代码redis-cli  --eval  unlock.script lock_key , unique_value

分布式锁的可靠性

为了避免锁实例出现故障而导致的锁无法工作的问题,需要按照一定的步骤和规定。Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。

RedLock的基本思路是,是让客户端和多个独立的Redis实例依次请求加锁,如果客户端能够与半数以上的实例成功地完成加锁操作,那么就认为客户端获得了分布式锁。这样一来,即使有某个Redis实例发生故障,那么也有其它实例可以做锁操作的支撑。

RedLock算法实现的可以分为3个步骤,假设需要有N个独立的Redis实例:

  • 客户端获取当前时间;
  • 客户端按顺序依次向N个Redis实例执行加锁操作;
    • 向Redis实例请求加锁,一样是采用SET NX 原子操作的命令,为了保障在加锁过程中Redis故障了,需要给加锁操作设置一个超时时间。如果超时了,那么会去下一个Redis实例继续请求加锁。
    • 加锁操作的超时时间需要远远小于锁的有效时间,一般也是设置几十毫秒。
  • 一旦客户端完成了和所有Redis实例的加锁操作,客户端计算整个加锁过程的总耗时。
    • 客户端需要满足以下两个条件才能认为是加锁成功:
    • 客户端从超过半数(大于等于N/2 + 1)的Redis实例上成功获取到了锁;
    • 客户端获取锁的总耗时没有超过锁的有效时间。

在满足加锁成功的条件后,需要重新计算锁的有效时间,计算结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,那么就需要释放锁,以免出现还没完成数据操作,锁就过期的情况。

本文转载自: 掘金

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

初识Java反射概念和使用

发表于 2021-11-08

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」

前言

  大家好,瑞雪后的第一天,每个周一的大家都期待这周五的来临。相信很多小伙伴上周末就两件事,赏雪和看EDG。哈哈 开始正题吧,今天聊一聊反射吧,在java中经常使用。

反射

  相信刚接触Java的,第一句肯定会问什么是反射呢?反射有什么作用呢?为什么使用反射呢?首先反射是Java的特征之一,项目中Java程序在运行的过程中,自动去识别并创建对应的类,能够动态的调用类的属性、构造方法、类中的方法。一句话描述:在程序中能动态调用不同的类和属性,去执行特定的操作。

  正因为反射能够在运行时动态加载需要的对象,所以很多框架中都使用到了反射。本次为了大家很好的理解反射,将基于反射创建对象、获取反射中的对象、获取类中属性、获取类中的构造方法、获取类中方法几个方面进行介绍,下面开始进入正题。

基础数据准备

  为了方便演示,创建了一个基础的对象类。并基于他进行本次的介绍。创建的演示类JueJinUser如下,包含四个属性,分别有get和set方法,还有toString方法,没有创建构造方法,下面会介绍通过反射自动创建的相关反射的方法。

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
js复制代码public class JueJinUser {
private Integer id;

private String name;

private String title;

private Integer age;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "JueJinUser{" +
"id=" + id +
", name='" + name + '\'' +
", title='" + title + '\'' +
", age=" + age +
'}';
}
}

基于反射创建对象

  基于反射创建类对象主要有两种方式:第一种是通过类对象的newInstance()方法创建对象,第二种是通过构造器中的 newInstance()方法创建对象。

类对象的newInstance()方法

  类对象的newInstance()方法创建方法如下:

1
2
js复制代码       Class class = JueJinUser.class;
JueJinUser jueJinUseByClass = (JueJinUser) class.newInstance();
构造器的newInstance()方法
1
2
3
js复制代码        Class jueJinUserClass = JueJinUser.class;
Constructor constructor = jueJinUserClass.getConstructor();
JueJinUser jueJinUserByConstructor = (JueJinUser) constructor.newInstance();

  需要注意的是,两种创建方式中第一种基于类对象的newInstance()的方法只能是无参构造方法,而第二种基于构造器的 newInstance()方法可以有有参构造方法和无参构造方法,比较灵活。

获取反射中的对象

  获取反射对象的方式有三种,分别是:Class.forName、.class 方法和getClass() 方法。获取反射中对象的方法大家在项目中使用的还是比较多的,相信大家都不陌生。

Class.forName
1
js复制代码    Class clzForName = Class.forName("com.example.demo.module.JueJinUser");
.class 方法
1
js复制代码    Class clzForClass =JueJinUserString.class;
getClass() 方法
1
2
js复制代码    JueJinUser JueJinUser = new JueJinUser();
Class clzNewObject = str.getClass();

获取类中属性

  通过getFields和getDeclaredFields的方法,可以获取类中的属性信息,其中getFields可以获取类中的公有属性值,而getDeclaredFields的方法,获取所有类中的属性信息,但是无法获取到父类的信息。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码        Class clz = JueJinUser.class;
Field[] fields = clz.getFields();
System.out.println("--- getFields start ---");
for (Field field : fields) {
System.out.println(field.getName());
}
System.out.println("--- getFields end ---");


System.out.println("--- getDeclaredFields start ---");
Field[] declaredFields = clz.getDeclaredFields();
for (Field field : declaredFields) {
System.out.println(field.getName());
}
System.out.println("--- etDeclaredFields end ---");

获取类中的构造方法

  通过getConstructors和getDeclaredConstructors的方法,可以获取类中的构造方法信息,其中getConstructors可以获取类中构造方法,而getDeclaredConstructors的方法,获取所有类中的构造方法,但是无法获取到父类的构造方法信息。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码     System.out.println("--- getConstructors start ---");
Constructor[] constructors = clz.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor.getName());
}
System.out.println("--- getConstructors end ---");

System.out.println("---getDeclaredConstructors start---");
Constructor[] declaredConstructors = clz.getDeclaredConstructors();
for (Constructor constructor : declaredConstructors) {
System.out.println(constructor.getName());
}
System.out.println("---getDeclaredConstructors end---");

获取类中方法

  通过getMethods和getDeclaredMethods的方法,可以获取类中的构造方法信息,其中getMethods可以获取类中构造方法,而getDeclaredMethods的方法,获取所有类中的方法,但是无法获取到父类的方法信息。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码
System.out.println("--- getMethods start ---");
Method[] methods = clz.getMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
System.out.println("--- getMethods end ---");


System.out.println("--- getDeclaredMethods start---");
Method[] declaredMethods = clz.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println(method.getName());
}
System.out.println("--- getDeclaredMethods end ---");

结语

  好了,以上就是反射的简单介绍,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

MyBatis TypeHandler学习及实战

发表于 2021-11-08

MyBatis在金融领域的使用非常广泛,作为一款优秀的ORM框架,其独特的优势在于:

①对于sql的分离,使业务开发同事更专注于逻辑实现;

②MyBatis对存储过程的有很好的支持(特别是对一些时间延时要求高,业务较为稳定的场景)。

作为一款ORM框架,不同的系统之间数据类型的转换就是首先要考虑的;MyBatis一项基本的功能,就是帮助我们,将JDBC类型与Java类型之间转换变得透明,能解放大家更多得精力去与产品吵架(狗头)。开个玩笑,正是由于JDBC类型与Java类型,并不是一对一得关系,所以我们更加需要MyBatis去帮我们解决这类繁琐而且重复的工作。

TypeHandler,就是MyBatis给出的解决方案。

TypeHandler简述

MyBatis在设置预处理语句(PreparedStatement)中的参数,或从结果集中取出一个值时, 会选择使用对应的TypeHandler类型处理器,将获取到的值以合适的方式转换成 Java 类型;MyBatis的在内部设定了很多基础的处理器,下图是从MyBatis代码中截取的,框架内部封装好的类型处理器。
image-20211028123441030.png

TypeHandler使用

在MyBatis框架中,为了方便开发人员,提供了三种方式使用TypeHandler:

  1. 从config文件中,通过typeHandlers以及子标签typeHandler,对自定义的TypeHandler进行注册;
  2. 或者通过package子标签,对TypeHandler所在包进行扫描注册;
  3. 在mapper.xml文件中,在resultMap或者在参数使用中,可以显示的声明TypeHandler,如下:
1
2
3
4
5
6
7
8
bash复制代码<result column="phone" property="phone" 			 
typeHandler="com.example.typeHandle.ClientPhoneTypeHandler"/>

<update id="updatePhone">
update users
set phone = #{phone, typeHandler=com.example.typeHandle.ClientPhoneTypeHandler}
where id = #{id}
</update>
  1. spring boot集成了MyBatis的starter,可以通过*.properties的文件进行配置
1
ini复制代码mybatis.type-handlers-package=${packagePath}

PS:笔者并未找到可以通过单个注册的处理器的方法

TypeHandler原理

那么TypeHandler在整个生命周期中是如何加载及使用的呢?

首先是注册阶段,MyBatis会通过XMLConfigBuilder对配置的xml文件中的标签进行解析,其中就包含上文中提到的TypeHandler相关标签;在解析时,会调用TypeHandlerRegistry的注册方法,将自定义的handler加载到JVM中;如果大家有兴趣,点开这个注册类,就能发现它的构建方法中,将上文列举的一些MyBatis框架内部定义的基础handler,进行了统一的初始化,并用Map将其与JDBC的类型建立的关联;

接下来,在解析mapper文件时,XMLMapperBuilder会选择适当的处理器;在解析ResultMap和ParameterMap时,从上阶段注册的TypeHandler中,找到最合适的TypeHandler,或者从mapper文件中,读取显示声明的TypehHandler,存入ParameterMapping实例中;

最终语句执行阶段,会调用处理器,对参数或查询结果进行转换;对应的Sql语句在输入参数时,调用TypeHander接口的setParameter方法,进行入参转换操作;获取到查询结果后,会调用getResult的方法,对结果转换从而返回。

自己定义一个TypeHandler

用两个实际的例子,来分享下我的使用场景。

场景1,用户更新标识;
要求:公司内部有单独的人力系统维护人员信息,笔者所在系统的人员信息修改有三种方式:1、员工手动修改;2、管理员手动修改;3、来自行内人力资源系统信息。业务规定员工手动修改信息后,不再同步来自人力资源系统的修改信息;同时需要根据修改的信息,设置同步标志,每一种不同的信息,必须通过不同的标识位进行控制。

设计与实现:人员主表增加更新标识位字段,通过二进制位,标识每个不同种类的信息的是否同步;代码中增加专用的DTO类,表明该数据为特殊更新属性标识;新增注解和二进制位枚举,通过在DTO类属性添加注解,指定该属性对应的枚举信息;增加专用TypeHandler用于数据库中的number类型与DTO类型的转换;在mapper文件中,显示的指定具体的TypeHandler。

Talk is cheap, show me your code.

注解类:

1
2
3
4
5
6
7
8
less复制代码@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MappedPropertiesChangeType {

PersonDetailChangeEnum value();

String name();
}

DTO类:

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
ini复制代码public class PropertiesUpdateMarkDTO implements Serializable {

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.EMAIL,
name = "email")
private int email = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.PHONE,
name = "phone")
private int phone = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.ICON,
name = "icon")
private int icon = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.FIXTEL,
name = "fixTel")
private int fixTel = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.DUTY,
name = "duty")
private int duty = 0;

private static class Constant {
private static final Field[] _FIELDS = PropertiesUpdateMarkDTO.class.getDeclaredFields();
}

/** 代码总条数过长,故 getter 和 setter 方法省略 **/

public static int transformDtoToInt(PropertiesUpdateMarkDTO dto) {

return Arrays.stream(Constant._FIELDS).map(
field -> PropertiesUpdateMarkDTO.bitToInt(field, dto)
).reduce(0, Integer::sum);
}

public static PropertiesUpdateMarkDTO transformIntToDTO(int input) {

PropertiesUpdateMarkDTO dto = new PropertiesUpdateMarkDTO();
Arrays.stream(Constant._FIELDS).forEach(field -> setField(field, dto, input));
return dto;
}

private static int bitToInt(Field field, PropertiesUpdateMarkDTO dto) {

MappedPropertiesChangeType[] annotations
= field.getAnnotationsByType(MappedPropertiesChangeType.class);

if (annotations.length > 1) {
throw new EbdcException(EbdcErrorCode.Biz.B0001,
"Do not support multiple MappedPropertiesChangeType annotation");
}

Method method = BeanUtil.getPropertyDescriptor(
PropertiesUpdateMarkDTO.class,
annotations[0].name()
).getReadMethod();
return ((Integer) ReflectUtil.invoke(dto, method))
<< annotations[0].value().getDisposition();
}

private static void setField(Field field, PropertiesUpdateMarkDTO dto, int input) {

MappedPropertiesChangeType[] annotations
= field.getAnnotationsByType(MappedPropertiesChangeType.class);

if (annotations.length > 1) {
throw new EbdcException(EbdcErrorCode.Biz.B0001,
"Do not support multiple MappedPropertiesChangeType annotation");
}

Method method = BeanUtil.getPropertyDescriptor(
PropertiesUpdateMarkDTO.class,
annotations[0].name()
).getWriteMethod();
ReflectUtil.invoke(dto, method,
(input & annotations[0].value().getBinary())
>> annotations[0].value().getDisposition());
}

}

Enum类:

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
csharp复制代码public enum PersonDetailChangeEnum {

EMAIL(0),//邮箱
PHONE(1),//移动电话
ICON(2),//头像
FIXTEL(3),//固定电话
DUTY(4);//职务

private final int binary;

private final int disposition;

public Integer getBinary() {
return binary;
}

public int getDisposition() {
return disposition;
}

PersonDetailChangeEnum(int disposition) {
this.binary = 1 << disposition;
this.disposition = disposition;
}

public static Integer changeBinaryState(List<Integer> integers, Integer result) {
if (result == null) {
result = 0;
}
for (Integer value : integers) {
result = value | result;
}
return result;
}
}

TypeHandler实现:

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复制代码@MappedTypes(PropertiesUpdateMarkDTO.class)
@MappedJdbcTypes(JdbcType.NUMERIC)
public class PropertiesMarkHandler implements TypeHandler<PropertiesUpdateMarkDTO> {

@Override
public void setParameter(PreparedStatement preparedStatement, int i,
PropertiesUpdateMarkDTO propertiesUpdateMarkDTO, JdbcType jdbcType) throws SQLException {
preparedStatement.setInt(i,
PropertiesUpdateMarkDTO.transformDtoToInt(propertiesUpdateMarkDTO));
}

@Override
public PropertiesUpdateMarkDTO getResult(ResultSet resultSet, String s) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(resultSet.getInt(s));
}

@Override
public PropertiesUpdateMarkDTO getResult(ResultSet resultSet, int i) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(resultSet.getInt(i));
}

@Override
public PropertiesUpdateMarkDTO getResult(CallableStatement callableStatement, int i) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(callableStatement.getInt(i));
}
}

mapper.xml文件:

1
2
3
4
5
ini复制代码<resultMap id="HrsPersonVOResultMap" type="*.person.bo.PersonVO" extends="PersonVOResultMap">
<result column="JOB_NAME" property="jobName"/>
<result column="PROPERTIES_UPDATE_MARK" property="properties"
typeHandler="*.PropertiesMarkHandler"/>
</resultMap>

场景2,银行卡号脱敏展示;

要求:用于展示用户银行卡账号的页面,为了保证客户信息不被泄露,都必须经过脱敏处理的,显示带有*的银行卡号;

设计与实现:增加特殊的String字段的TypeHandler类;在mapper中,找到需要脱敏的DO中字段,同时在对应的ResultMap中,显示的指定该TypeHandler;数据库读取到的银行卡账号信息,通过getNullableResult方法,进行转换,从而达到脱敏处理。

TypeHandler实现:

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复制代码public class DesCardNoTypeHandler extends BaseTypeHandler<String> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
}

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return desCardNo(rs.getString(columnName));
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return desCardNo(rs.getString(columnIndex));
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return desCardNo(cs.getString(columnIndex));
}

private static String desCardNo(String cardNo) {
return DesensitizedUtil.bankCard(cardNo);
}
}

ResultMap信息:

1
2
3
4
5
6
7
8
9
ini复制代码<resultMap id="BaseUserCardInfo" type="com.example.entity.card.UserCard">
<id property="userId" column="user_id"/>
<result property="cardNo" column="card_no"/>
</resultMap>

<resultMap id="DesUserCardInfo" type="com.example.entity.card.UserCard">
<id property="userId" column="user_id"/>
<result property="cardNo" column="card_no" typeHandler="com.example.typeHandle.DesCardNoTypeHandler"/>
</resultMap>

运行结果:

1
2
3
4
5
6
7
8
ini复制代码Created connection 2006112337.
==> Preparing: select user_id, card_no from user_card where user_id = ?
==> Parameters: 1(String)
<== Columns: user_id, card_no
<== Row: 1, 6666666666666666666
<== Total: 1
UserCard{userId='1', cardNo='6666 **** **** *** 6666'}
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7792d851]

本文转载自: 掘金

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

Go高阶20,定时器的使用 Timer Ticker

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

Timer

Timer 是一种单一事件定时器,就是说 Timer 只执行一次就会结束。

创建:

time.NewTimer(d Duration) :创建一个 timer

  • 参数为等待事件
  • 时间到来后立即触发一个事件

源码包 src/time/sleep.go:Timer 定义了Timer数据结构:

1
2
3
4
go复制代码type Timer struct {
C <-chan Time
r runtimeTimer
}

Timer 对外仅暴露了一个 channel,当指定时间到来就会往该 channel 中写入系统时间,即一个事件。

timer 使用

设定超时时间

比如在一个连接中等待数据,设定一个超时时间,当时间到来还是没有数据获取到,则为超时。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func WaitChannel(conn <-chan string) bool {
timer := time.NewTimer(3 * time.Second)

select {
case <- conn:
timer.Stop()
return true
case <- timer.C: //超时
fmt.Println("WaitChannel timeout")
return false
}
}

如上示例中,select 语句轮询 conn 和 timer.C 两个管道,timer
会在 3s 后向 timer.C 写入数据,如果 3s 内 conn 还没有数据,则会判断为超时。

延迟执行方法

有时我们希望某个方法在今后的某个时刻执行:

1
2
3
4
5
6
7
8
go复制代码func DelayFunction() {
timer := time.NewTimer(5 * time.Second)

select {
case <- time.C
fmt.Println("Delayed 5s,...")
}
}

DelayFunction()会一直等待timer的事件到来才会执行后面的方法(打印)。

Timer对外接口

创建定时器

func NewTimer(d Duration) *Timer :

  • 指定一个时间即可创建一个 Timer,Timer 一经创建便开始计时,不需要额外的启动。
  • 创建 Timer 意味着把一个计时任务交给系统守护协程,该协程管理着所有的 Timer,当 Timer 的时间到达后向 Timer 的管道中发送当前的时间作为事件。

停止定时器

func (t *Timer) Stop() bool :

  • Timer 创建后可随时停止
  • 返回值表示是否超时:
    • true : 定时器未超时,后续不会再有事件发送
    • false : 定时器超时后停止

重置定时器

func (t *Timer) Reset(d Duration) bool:

  • 已经过期的定时器或已经停止的定时器,可以通过重置来重新激活
  • 重置的动作实质上是先停掉定时器,再启动。其返回值也即停掉计时器的返回值。

简单接口

After()

有时我们就是想等指定的时间,没有需求提前停止定时器,也没有需求复用该定时器,那么可以使用匿名的定时器:

1
2
3
4
5
go复制代码func AfterDemo(){
log.Println(time.Now)
<- time.After(1 * time.Second)
log.Println(time.Now())
}

打印时间间隔为1s,实际还是一个定时器,但代码变得更简洁。

AfterFunc()

我们可以使用 AfterFunc 更加简洁的实现延迟一个方法的调用:

func AfterFunc(d Duration, f func()) *Timer

示例:

1
2
3
4
5
6
7
8
go复制代码func AfterFuncDemo() {
log.Println("AfterFuncDemo start", time.Now())
time.AfterFunc(1 * time.Second, func(){
log.Println("AfterFuncDemo end", time.Now())
})

time.Sleep(2 * time.Second) //等待协程退出
}
  • AfterFuncDemo()中先打印一个时间,然后使用 AfterFunc 启动一个定时器,并指定定时器结束时执行一个方法打印结束时间。
  • time.AfterFunc()是异步执行的,所以需要在函数最后sleep等待指定的协程退出,否则可能函数结束时协程还未执行。

Ticker

Ticker是周期性定时器,即周期性的触发一个事件。其数据结构和 Timer 完全一致:

1
2
3
4
go复制代码type Timer struct {
C <-chan Time
r runtimeTimer
}

在创建Ticker时会指定一个时间,作为事件触发的周期。这也是Ticker与Timer的最主要的区别。

Ticker 使用

定时任务

示例,每隔1s记录一次日志:

1
2
3
4
5
6
7
8
go复制代码func TickerDemo() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for range ticker.C {
log.Println("ticker...")
}
}

for range ticker.C 会持续从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据会阻塞等待事件,由于 ticker 会周期性的向管道中写入事件,所以上述程序会周期性的打印日志。

Ticker对外接口

创建定时器

func NewTicker(d Durtion) * Ticker :

  • 参数 d 为定时器事件触发的周期。

停止定时器

func (t * Ticker) Stop() :

  • 该方法会停止计时,意味着不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完成后,生命周期结束后会自动释放。
  • Ticker 在使用完后务必要释放,否则会产生资源泄露,进而会持续消耗CPU资源,最后会把CPU耗尽。

简单接口

如果我们需要一个定时轮询任务,可以使用一个简单的Tick函数来获取定时器的管道,函数原型如下:

func TIck(d Durtion) <-chan Time :

  • 这个函数内部实际还是创建一个 Ticker,但并不会返回出来,所以没有手段来停止该 Ticker。所以,一定要考虑具体的使用场景。

错误示例

1
2
3
4
5
6
7
8
go复制代码func WorngTicker() {
for {
select {
case <- time.Tick(1 * time.Second)
log.Println("资源泄露")
}
}
}

如上错误示例,select 每次检测 case 语句时都会创建一个定时器,for 循环又会不断的执行select语句,所以系统里会有越来越多的定时器不断的消耗 CPU 资源,最终CPU会被耗尽。

正确用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func demo(t interface{}) {
for {
select {
case <-t.(*time.Ticker).C:
println("1s timer")
}
}
}

func main() {
t := time.NewTicker(time.Second * 1)
go demo(t)
select {}
}

本文转载自: 掘金

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

spring循环依赖-不仅仅是八股文 一前言 二bug缘

发表于 2021-11-08

一.前言

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

hello,everyone。

spring的循环依赖问题是面试的时候经常会碰到的问题。相信很多朋友都看相关spring使用三级缓存解决循环依赖的博文。面试官问你的时候除了想要了解你对spring框架的熟悉程度,还想要了解你对spring循环依赖的思考。

你上来直接说spring使用了三级缓存解决了循环依赖,那你就要回家等通知了。

前几天写需求的时候,整合了几个方法逻辑的时候,碰到了一个循环依赖的bug。

借着这个bug的排查思路给大家讲讲spring循环依赖中几个小坑。

本文重点并非spring循环依赖源码解读,默认你对spring循环依赖有过简单的了解。

我贴心点吧,贴一下大神A哥的blog:一文告诉你Spring是如何利用“三级缓存“巧妙解决Bean的循环依赖问题的

二.bug缘由

博主在进行一个需求开发的时候,需要调用几个现有接口的逻辑,但是它们原先的方法是私有的,并且好几个逻辑定义在controller层【历史原因,强烈谴责此种做法!】。

为了方法复用,我将controller中对应通用逻辑进行剥离同步到对应的service中,并且注入了相关依赖的一些bean。

然后代码结构变成了,serviceA注入了serviceB,serviceB注入了serviceA

然后项目一启动就报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'appManagerServiceImpl': Bean with name 'appManagerServiceImpl' has been injected into other beans [deviceManagerServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
​
​
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:623)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657)
... 26 common frames omitted

这个其实我当时还挺诧异的,我在不看绝对后悔的@Async深度解析【不仅仅是源码那么简单】这篇文章中4.3提到过使用@Async,互相注入bean会导致循环依赖。

image-20211108195622193.png

但是我在这两个bean中全局搜索@Aysnc,也没有搜索到。

然后也没有看到构造器注入的场景。

so,看来还是只能调一下源码吧。

三.bug定位

看到这个bug,直接定位到堆栈报出来的错误行

image-20211108194749448.png
报错比较简单直观,暴露对象与关联的bean不是同一个对象,在这里打一个条件断点:

exposedObject != bean

image-20211108195849145.png

image-20211108200421743.png

发现bean是原始对象,exposedObject是代理对象。

借用一下A哥的图

2019061918335746.png

spring解决循环依赖时,beanB去获取beanA时,beanA如果切面处理,那么beanB关联beanA时,会调用

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  Object exposedObject = bean;
  if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
          SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
          exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
  }
  return exposedObject;
}

生成代理对象,将beanA从三级缓存中删除,生成代理对象放置到二级缓存中。

但是由于getEarlyBeanReference方法中仅对类型为SmartInstantiationAwareBeanPostProcessor的后置处理器进行代理处理。如果是其他的类型的BeanPostProcessor,将不会在此处做增强。

ok,我们再回过头看一下上面的流程图,bean加载最后的逻辑在

1
ini复制代码exposedObject = initializeBean(beanName, exposedObject, mbd);

这一行,最后处理bean的逻辑

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复制代码protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
  if (System.getSecurityManager() != null) {
    AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
        invokeAwareMethods(beanName, bean);
        return null;
    }, getAccessControlContext());
  }
  else {
    invokeAwareMethods(beanName, bean);
  }
​
  Object wrappedBean = bean;
  //后置处理器前置处理
  if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
  }
​
  try {
    invokeInitMethods(beanName, wrappedBean, mbd);
  }
  catch (Throwable ex) {
    throw new BeanCreationException(
          (mbd != null ? mbd.getResourceDescription() : null),
          beanName, "Invocation of init method failed", ex);
  }
  //后置处理器后置处理
  if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
  }
​
  return wrappedBean;
}

分别打断点在这里

image-20211108202317887.png

能够看到这两行的wrappedBean对象不一样,一个是原始对象,换一个是代理对象。

看到曙光,再进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
    throws BeansException {
​
  Object result = existingBean;
  for (BeanPostProcessor processor : getBeanPostProcessors()) {
    Object current = processor.postProcessAfterInitialization(result, beanName);
    if (current == null) {
        return result;
    }
    result = current;
  }
  return result;
}

断点一打

image-20211108202940526.png

逐个排查

image.png

发现bean被MethodValidationPostProcessor增强处理了!!!!

点击这个类进去,被标注了@Validated的类将被代理增强。

四.bug解决

bug解决就很简单了,发现在service上标注了 @Validated,为了校验方法入参。

我就简单粗暴,把校验逻辑写了一个方法单独处理。

五.总结

其实这篇文章的排查思路跟@Async那篇文章的排查思路是一模一样的,但是我在排查的时候有增加了不少的思考。

1.spring本身帮助我们解决了属性注入方式的循环依赖。但是如果循环依赖的bean,被除SmartInstantiationAwareBeanPostProcessor的后置处理器代理到,那么还是会产生循环依赖的报错。

2.spring无法为我们解决构造器循环依赖,因为三级缓存的最开始操作就是要对bean实例化放入到三级缓存。

3.使用 @Lazy去解决类似本文的这种bug,是可行的。比如B希望依赖进来的是最终的代理对象进来,所以B加上即可,A上并不需要加。但是实际上,此种情况下B里持有A的引用和Spring容器里的A并不是同一个。 【本强迫症患者看来,治标不治本】

4.其实二级缓存也能解决注入循环依赖,但是为什么要使用三级缓存?spring还是期望bean的声明周期是符合spring的设计规范的,类似于二级缓存的早期曝光提前生成代理的方式,是为了系统的健壮性考虑。

5.谨慎使用:allowRawInjectionDespiteWrapping,把这个置为true后会针对循环内的bean不进行校验,但是代理会失效了。

六.联系我

文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~

钉钉:louyanfeng25

微信:baiyan_lou

公众号:柏炎大叔

image.png

本文转载自: 掘金

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

Python 可视化之matplotlib模块浅析 前言 1

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

前言

互联网时代下,在网络中每天都会产生很多数据,通过对数据分析之后,如何更好的诠释数据背后的意义,我们需要对数据进行可视化展示。

在数据可视化中,Python 也支持第三模块

  • matplotlib 模块:Python使用最多的可视化库
  • seaborn 模块:基于matplotlib的图形可视化
  • pycharts 模块:用于生成Echarts 图表的类库

image.png

本期,我们对matplotlib模块提供的图形方法进行学习,Let’s go~

  1. matplotlib 模块概述

matplotlib 模块是第三方开源的,由John Hunter团队研发而成,NumFOCUS 的赞助项目。

matplotlib 模块是用于Python创建静态、动态和交互式可视化综合性的库。

image.png

  • matplotlib 模块特点

+ 易创建图表如出版质量图、交互式数据可放大、缩小
+ 定制化图表可完全控制线条样式、导入并嵌入多种文件格式
+ 扩展性高,可以与第三方模块进行兼容
+ matplotlib 模块资料手册信息丰富,可快速上手
  • matplotlib 模块获取

matplotlib 是Python主流第三方可视化模块,我们需要使用pip进行下载

1
js复制代码pip install matplotlib
  • matplotlib 模块使用

在matplotlib模块中,pyplot类是最常用的。

+ 方式一:
1
python复制代码from matplotlib import pyplot
+ 方式二:
1
python复制代码import matplotlib.pyplot as plt

🔔 重要说明

  1. matplotlib 模块官方资料
  2. 查看matplotlib内部代码说明

image.png

  1. matplotlib.pyplot 相关方法

matplotlib.pyplot 模块是我们画图标最常用的模块之一

方法 作用
pyplot.title(name) 图表的标题
pyplot.xlabel(name) 图表的X轴名字
pyplot.ylabel(name) 图表的y轴名字
pyplot.show() 打印出图表
pyplot.plot(xvalue,yvalue) 绘制折线图表
pyplot.bar(xvalue,yvalue) 绘制柱状图表
pyplot.axis(data) 获取或设置一些轴属性的便捷方法
pyplot.scatter(data) 绘制散点图
pyplot.subplot(data) 绘制子图
pyplot.grid(boolean) 显示网状,默认为False
pyplot.text() 对文本进行处理
pyplot.pie(data) 绘制饼图
pyplot.boxplot(data) 绘制箱形图
pyplot.hist(data) 绘制直方图
  1. matplotlib.pyplot 图表展示

  • 绘制折线图

+ 使用pyplot..plot()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
Python复制代码from matplotlib import pyplot

# 设置图表字体格式
pyplot.rcParams["font.sans-serif"]=['SimHei']
pyplot.rcParams["axes.unicode_minus"]=False

pyplot.plot([1,2,3,4,5,6],[45,20,19,56,35,69])

pyplot.title("data analyze")
pyplot.xlabel("data")
pyplot.ylabel("sum")

pyplot.show()

image.png

  • 绘制柱状图

+ 使用pyplot..bar()方法
+ 再次使用上面的数据,可以看到直方图
1
js复制代码pyplot.bar([1,2,3,4,5,6],[45,20,19,56,35,69])

image.png

  • 绘制饼图

+ 使用pyplot.pie()方法绘制饼图
+ 同时使用pyplot.axis方法设置每一个分区间隔
1
2
3
4
5
6
7
8
9
python复制代码from matplotlib import pyplot
labels = ["windows","MAC","ios","Android","other"]
sizes = [50,10,5,15,20]
explode = [0,0.1,0,0,0]
pyplot.pie(sizes,explode=explode,labels=labels,autopct='%1.1f%%',shadow=False,startangle=90)
pyplot.axis("equal")

pyplot.title("data analyze")
pyplot.show()

image.png

  • 绘制散点图

+ 使用pyplot.scatter(x,y)绘制散点图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import numpy as np
from matplotlib import pyplot

data = {"a":np.arange(50),"c":np.random.randint(0,50,50),"d":np.random.randn(50)}

data['b'] = data['a']+10*np.random.randn(50)
data['d'] = np.abs(data['d'])*100

pyplot.scatter("a","b",c='c',s='d',data=data)

pyplot.title("data analyze")
pyplot.xlabel("元素 a")
pyplot.ylabel("元素 b")

pyplot.show()

image.png

总结

本期,我们对matplotlib.pyplot 模块绘制相关如折线、柱状、散点、圆饼图表进行简单地学习

在学习的过程中,我们发现pyplot 模块简单上手,发现在展示之前我们所有的数据是关键点

以上是本期内容,欢迎大佬们点赞评论,我们下期见~

本文转载自: 掘金

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

mysql 日志文件

发表于 2021-11-08

这是我参与11月更文挑战的第 7 天,活动详情查看:2021最后一次更文挑战

日志文件记录了影响 mysql 数据库的各种类型活动,mysql 中常见的日志文件主要包括以下 4 种:

  • 错误日志
  • 二进制日志
  • 慢查询日志
  • 查询日志

这些日志文件可以帮助我们对mysql数据库的运行状态进行诊断,从而更好的进行数据库层面的优化。

错误日志

错误日志文件对 mysql 的启动,运行,关闭过程进行了记录。错误日志不仅记录了所有的错误信息,也记录了一些警告信息或者正确的信息。可以通过以下方式 找到错误日志的路径:

1
2
3
4
5
6
7
sql复制代码mysql> show variables like 'log_error';
+---------------+----------------------------------+
| Variable_name | Value |
+---------------+----------------------------------+
| log_error | ./weihengdeMacBook-Pro.local.err |
+---------------+----------------------------------+
1 row in set (0.00 sec)

可以看到错误日志的全路径,这里我本机没有做过调整,所以默认使用的是主机名作为错误日志的文件名。如果你的 mysql 数据库不能正常启动,可以第一时间查找的文件就是错误日志文件,这里可能会记录相关的错误日志,通过这个日志来找到不能正常启动的原因。下面截取部分日志:

1
2
3
4
5
6
ini复制代码2021-05-06T08:57:03.359102Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-05-06T08:57:03.439761Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2021-05-06T08:57:03.495740Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '127.0.0.1' port: 33060, socket: /tmp/mysqlx.sock
2021-05-06T08:57:03.521389Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2021-05-06T08:57:03.521511Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
2021-05-06T08:57:03.529529Z 0 [System] [MY-010931] [Server] /opt/homebrew/Cellar/mysql/8.0.23_1/bin/mysqld: ready for connections. Version: '8.0.23' socket: '/tmp/mysql.sock' port: 3306 Homebrew.

慢查询日志

long_query_time

mysql 启动时可以设定一个阈值,mysql 会将运行时间超过该值的所有 sql语句都记录到慢查询日志中。如下:

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.02 sec)

mysql> show variables like 'log_slow_queries';
Empty set (0.01 sec)

设置 long_query_time 阈值之后,mysql 数据库会记录运行时间超过该阈值的所有 sql 语句,但是对于时间刚好等于 long_query_time 值的不会被记录。从 mysql 5.1 开始,long_query_time开始以微秒记录 sql 语句运行的时间,在此之前都是用秒作为单位记录的。

log_queries_not_using_indexes

这是另一个和慢查询有关的参数,这个参数表示,如果运行的 sql 语句没有使用索引,则 mysql 数据库同样会将这条 sql 语句记录到慢查询日志文件。

1
2
3
4
5
6
7
sql复制代码mysql> show variables like 'log_queries_not_using_indexes';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | OFF |
+-------------------------------+-------+
1 row in set (0.00 sec)

log_throttle_queries_not_using_indexes

1
2
3
4
5
6
7
sql复制代码mysql> show variables like 'log_throttle_queries_not_using_indexes';
+----------------------------------------+-------+
| Variable_name | Value |
+----------------------------------------+-------+
| log_throttle_queries_not_using_indexes | 0 |
+----------------------------------------+-------+
1 row in set (0.01 sec)

mysql 5.6.5 版本开始新增了 log_throttle_queries_not_using_indexes 这个参数,用来表示每分钟允许记录到 slow log 的且未使用索引的 sql 语句次数。该值默认是0,表示没有限制。在生产环境,如果没有使用索引,此类 sql 语句会频繁的被记录到 slow log,从而导致 slow log 文件不断增加,因此可以通过配置该值进行控制。

本文转载自: 掘金

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

魔术索引 LeetCode刷题笔记 二、思路分析 三、A

发表于 2021-11-08

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」


相关文章

LeetCode刷题汇总:LeetCode刷题

一、题目描述


魔术索引

魔术索引。 在数组A[0…n-1]中,有所谓的魔术索引,满足条件A[i] = i。给定一个有序整数数组,编写一种方法找出魔术索引,若有的话,在数组A中找出一个魔术索引,如果没有,则返回-1。若有多个魔术索引,返回索引值最小的一个。

二、思路分析


  • 看看题目的示例,我们来理一理这个思路~
  • 示例 1:
1
2
3
ini复制代码 输入:nums = [0, 2, 3, 4, 5]
输出:0
说明: 0下标的元素为0
  • 示例2:
1
2
ini复制代码 输入:nums = [1, 1, 1]
输出:1
  • 说明
+ nums长度在[1, 1000000]之间
+ 此题为原书中的 Follow-up,即数组中可能包含重复元素的版本
  • 老规矩,先跟着第一感觉走,暴力破解来一波!

三、AC 代码


  • 暴力破解:
1
2
3
4
5
6
7
8
9
10
arduino复制代码class Solution {
   public int findMagicIndex(int[] nums) {
       for(int i=0;i<nums.length;i++){
           if(i==nums[i]){
               return i;
          }
      }
       return -1;
  }
}
  • 执行结果:
  • image-20211108204312974.png

这玩意应该不用解释了吧?

  • 跳跃寻找法:
+ 我们可以看看题目的示例,可以理解为这个数组是`递增`、`可重复`的。
+ 也就是 num[i] 一定大于等于 num[i-1] 的。
+ 那么这样我们是不是可以理解为:当num[i] > i(即当前下标),那么一直到这个num[i] 这个数都不符合条件?
+ 举例:


    - num[] = {1,2,3,8,9,11};
    - num[1] = 2 > 1
    - num[2] = 3 : 这个3肯定是大于2的吧?也就是百分百不符合条件
    - 以此类推
    - num[3]=8 >3 :是不是一直到num[8]都不会有符合条件的出现?
+ 完美,用这种方法试试看:
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
css复制代码class Solution {
      public int findMagicIndex(int[] nums) {
       int i = 0;
       while (i < nums.length) {
           if (nums[i] == i) {
               return i;
          } else if (nums[i] > i) {
               //当nums[i] > i,进行跳跃,如果i>nums.length证明无符合条件,返回-1
               i = nums[i];
          }else{
               i++;
          }
      }
       return -1;
  }
}
+ 执行结果: + ![image-20211108205708389.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/bd7dd4b9c38cb2bd5f875401c768388586ad83c7e1c3cebc51b55ffca399054e)

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

1…393394395…956

开发者博客

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