简栈文化

Java技术人的成长之路~

获取mysql镜像

1
2
3
4
➜  ~  docker pull mysql
➜ ~ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql latest d435eee2caa5 3 weeks ago 456MB

启动mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  ~  docker run -itd --name docker-mysql-master -v /Users/chenyuan/Data/docker/mysql-data-master:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -p 33061:3306 mysql
88820868af121cbac02f48a8c8e5c9eae5c6cf7241eefd3646634e14526a940f
➜ ~ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
88820868af12 mysql "docker-entrypoint.s…" About a minute ago Up About a minute 33060/tcp, 0.0.0.0:33061->3306/tcp docker-mysql-master
➜ ~ docker exec -it 88820868af12 bash
root@88820868af12:/#
root@88820868af12:/# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.18 MySQL Community Server - GPL

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

确定挂载的mysql-data文件

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
➜  mysql-data-master  ll
total 356592
drwxr-x--- 12 chenyuan staff 384B Dec 19 17:46 #innodb_temp
-rw-r----- 1 chenyuan staff 1.2K Dec 19 17:46 88820868af12.err
-rw-r----- 1 chenyuan staff 56B Dec 19 17:45 auto.cnf
-rw-r----- 1 chenyuan staff 2.9M Dec 19 17:46 binlog.000001
-rw-r----- 1 chenyuan staff 155B Dec 19 17:46 binlog.000002
-rw-r----- 1 chenyuan staff 32B Dec 19 17:46 binlog.index
-rw------- 1 chenyuan staff 1.6K Dec 19 17:45 ca-key.pem
-rw-r--r-- 1 chenyuan staff 1.1K Dec 19 17:45 ca.pem
-rw-r--r-- 1 chenyuan staff 1.1K Dec 19 17:45 client-cert.pem
-rw------- 1 chenyuan staff 1.6K Dec 19 17:45 client-key.pem
-rw-r----- 1 chenyuan staff 5.3K Dec 19 17:46 ib_buffer_pool
-rw-r----- 1 chenyuan staff 48M Dec 19 17:46 ib_logfile0
-rw-r----- 1 chenyuan staff 48M Dec 19 17:45 ib_logfile1
-rw-r----- 1 chenyuan staff 12M Dec 19 17:46 ibdata1
-rw-r----- 1 chenyuan staff 12M Dec 19 17:46 ibtmp1
drwxr-x--- 8 chenyuan staff 256B Dec 19 17:46 mysql
-rw-r----- 1 chenyuan staff 29M Dec 19 17:46 mysql.ibd
drwxr-x--- 105 chenyuan staff 3.3K Dec 19 17:45 performance_schema
-rw------- 1 chenyuan staff 1.6K Dec 19 17:45 private_key.pem
-rw-r--r-- 1 chenyuan staff 452B Dec 19 17:45 public_key.pem
-rw-r--r-- 1 chenyuan staff 1.1K Dec 19 17:45 server-cert.pem
-rw------- 1 chenyuan staff 1.6K Dec 19 17:45 server-key.pem
drwxr-x--- 3 chenyuan staff 96B Dec 19 17:46 sys
-rw-r----- 1 chenyuan staff 12M Dec 19 17:46 undo_001
-rw-r----- 1 chenyuan staff 10M Dec 19 17:46 undo_002
➜ mysql-data-master pwd
/Users/chenyuan/Data/docker/mysql-data-master

Binlog配置

查看binlog日志的地方,通过命令查看。因为没有设置过所以看不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> mysql> show variables like '%datadir%';
Empty set (0.01 sec)

root@88820868af12:/etc/mysql# pwd
/etc/mysql
root@88820868af12:/etc/mysql# ls -l
total 12
drwxrwxr-x 1 root root 4096 Nov 23 01:48 conf.d
-rw-rw-r-- 1 root root 1174 Nov 23 01:48 my.cnf
-rw-r--r-- 1 root root 1469 Sep 20 09:04 my.cnf.fallback

# 后面设置好后,就能看到了。
mysql> show variables like '%datadir%';
+---------------+------------------------+
| Variable_name | Value |
+---------------+------------------------+
| datadir | /usr/local/mysql/data/ |
+---------------+------------------------+
1 row in set (0.00 sec)

当前容器提交为镜像

到这里遇到一个非常好玩的事情,就是获取的mysql镜像是一个非常干净的容器,常用的命令都没有。比如:yum、ifconfig、cat等。所以我需要把当前的容器打一个镜像包。并且docker run的时候要挂载一个本地的目录,避免待会儿需要上传一些工具包。

1
2
3
4
5
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
-a :提交的镜像作者;
-c :使用Dockerfile指令来创建镜像;
-m :提交时的说明文字;
-p :在commit时,将容器暂停。
1
2
3
4
5
6
7
8
➜  ~  docker commit -a "chengcheng222e@sina.com" -m "created by vernon" 88820868af12  mysql-versnon:v1
sha256:e9691f399c321ea221b48e6142e9501f0ee69964fa4be687ac189f8444d75d66
➜ ~ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql-versnon v1 e9691f399c32 13 seconds ago 456MB
mysql latest d435eee2caa5 3 weeks ago 456MB

➜ Tools docker run -itd --name docker-mysql-master -v /Users/chenyuan/Data/docker/mysql-data-master:/var/lib/mysql -v /Users/chenyuan/Tools:/root/tools -e MYSQL_ROOT_PASSWORD=root -p 33061:3306 mysql-versnon:v1

这里注意一下,因为docker pull mysql镜像中的shell里面命令太少了,非常的不方便。这里建议大家还是利用dockefile的方式或者是docker pull centos的方式来安装mysql,不然你会怀疑人生。

http://static.cyblogs.com/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191228233247.jpg

通过dockerfile的方式:

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
FROM centos:7
MAINTAINER 2019-09-27 chenyuan chengcheng222e@sina.com

# Linux lib
RUN yum install -y tar
RUN yum install -y unzip
RUN yum install -y initscripts

# Software space
RUN mkdir -p ~/tools/
COPY jdk1.8.0_45.tar.gz ~/tools/
COPY mysql-5.6.45-linux-glibc2.12-x86_64.tar.gz ~/tools/

# JDK
WORKDIR ~/tools/
RUN tar -zxvf jdk1.8.0_45.tar.gz
RUN mv jdk1.8.0_45 /opt/
RUN ln -s /opt/jdk1.8.0_45/bin/* /usr/local/sbin/
ENV JAVA_HOME /opt/jdk1.8.0_45
ENV JRE_HOME ${JAVA_HOME}/jre
ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib
ENV PATH ${JAVA_HOME}/bin:$PATH

# MySQL
RUN cd ~/tools/
RUN yum -y install numactl
RUN yum -y install libaio
RUN yum -y install pwgen
RUN yum install -y perl-Data-Dumper
RUN tar -zxvf mysql-5.6.45-linux-glibc2.12-x86_64.tar.gz
RUN mv mysql-5.6.45-linux-glibc2.12-x86_64 /usr/local/mysql
RUN groupadd mysql
RUN useradd -g mysql mysql
RUN chown -R mysql /usr/local/mysql
RUN chgrp -R mysql /usr/local/mysql

RUN cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
COPY my.cnf /etc/my.cnf
RUN /usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data
ENV PATH $PATH:/usr/local/mysql/bin
EXPOSE 3306

my.conf配置

修改my.conf的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqld]
user=mysql
default-storage-engine=INNODB
character-set-server=utf8
basedir = /usr/local/mysql
datadir = /usr/local/mysql/data
port = 3306
socket = /tmp/mysql.sock

server-id = 1
log-bin=mysql-bin

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

Binlog格式

Binlog的格式也有三种:STATEMENT、ROW、MIXED 。

  • STATMENT模式:基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。

    • 优点:不需要记录每一条SQL语句与每行的数据变化,这样子binlog的日志也会比较少,减少了磁盘IO,提高性能。
    • 缺点:在某些情况下会导致master-slave中的数据不一致(如sleep()函数, last_insert_id(),以及user-defined functions(udf)等会出现问题)
  • 基于行的复制(row-based replication, RBR):不记录每一条SQL语句的上下文信息,仅需记录哪条数据被修改了,修改成了什么样子了。

    • 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题。
    • 缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨。
  • 混合模式复制(mixed-based replication, MBR):以上两种模式的混合使用,一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,MySQL会根据执行的SQL语句选择日志保存方式。

设置Binglog

通过show variables的方式来查看binlog的一个实际情况。

1
2
3
4
5
6
7
8
9
10
11
12
mysql> show variables like '%log_bin%';
+---------------------------------+-------+
| Variable_name | Value |
+---------------------------------+-------+
| log_bin | OFF |
| log_bin_basename | |
| log_bin_index | |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+-------+
6 rows in set (0.00 sec)

从上面可以看出,没有开启binlog日志,那么我们接下来开启binlog。

在/etc/my.cnf里面开启binlog配置。

1
2
server-id = 1
log-bin=mysql-bin

再次查看:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> show variables like '%log_bin%';
+---------------------------------+---------------------------------------+
| Variable_name | Value |
+---------------------------------+---------------------------------------+
| log_bin | ON |
| log_bin_basename | /usr/local/mysql/data/mysql-bin |
| log_bin_index | /usr/local/mysql/data/mysql-bin.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+---------------------------------------+
6 rows in set (0.01 sec)

查看binlog_format

1
2
3
4
5
6
7
8
mysql> show variables like 'binlog_format';
+---------------+-----------+
| Variable_name | Value |
+---------------+-----------+
| binlog_format | STATEMENT |
+---------------+-----------+
1 row in set (0.00 sec)
# 默认的格式就是 STATEMENT

确定binlog日志文件

1
2
3
4
5
6
mysql> show binlog events;
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 1 | 120 | Server ver: 5.6.45-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+

创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> use test;
Database changed
mysql> show tables;
Empty set (0.01 sec)
mysql> CREATE TABLE `person` (
-> `id` int(11) DEFAULT NULL,
-> `first_name` varchar(20) DEFAULT NULL,
-> `age` int(11) DEFAULT NULL,
-> `gender` char(1) DEFAULT NULL
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8
-> ;
Query OK, 0 rows affected (0.04 sec)
INSERT INTO test.person (id, first_name, age, gender) VALUES (1, 'Bob', 25, 'M');
INSERT INTO test.person (id, first_name, age, gender) VALUES (2, 'Jane', 20, 'F');
INSERT INTO test.person (id, first_name, age, gender) VALUES (3, 'Jack', 30, 'M');
INSERT INTO test.person (id, first_name, age, gender) VALUES (4, 'Bill', 32, 'M');
INSERT INTO test.person (id, first_name, age, gender) VALUES (5, 'Nick', 22, 'M');
INSERT INTO test.person (id, first_name, age, gender) VALUES (6, 'Kathy', 18, 'F');
INSERT INTO test.person (id, first_name, age, gender) VALUES (7, 'Steve', 36, 'M');
INSERT INTO test.person (id, first_name, age, gender) VALUES (8, 'Anne', 25, 'F');

查看binlog

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
mysql> show binlog events in 'mysql-bin.000001';
+------------------+------+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+------+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 1 | 120 | Server ver: 5.6.45-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 1 | 386 | use `test`; CREATE TABLE `person` (
`id` int(11) DEFAULT NULL,
`first_name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`gender` char(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
| mysql-bin.000001 | 386 | Query | 1 | 465 | BEGIN |
| mysql-bin.000001 | 465 | Query | 1 | 619 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (1, 'Bob', 25, 'M') |
| mysql-bin.000001 | 619 | Xid | 1 | 650 | COMMIT /* xid=14 */ |
| mysql-bin.000001 | 650 | Query | 1 | 729 | BEGIN |
| mysql-bin.000001 | 729 | Query | 1 | 884 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (2, 'Jane', 20, 'F') |
| mysql-bin.000001 | 884 | Xid | 1 | 915 | COMMIT /* xid=15 */ |
| mysql-bin.000001 | 915 | Query | 1 | 994 | BEGIN |
| mysql-bin.000001 | 994 | Query | 1 | 1149 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (3, 'Jack', 30, 'M') |
| mysql-bin.000001 | 1149 | Xid | 1 | 1180 | COMMIT /* xid=16 */ |
| mysql-bin.000001 | 1180 | Query | 1 | 1259 | BEGIN |
| mysql-bin.000001 | 1259 | Query | 1 | 1414 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (4, 'Bill', 32, 'M') |
| mysql-bin.000001 | 1414 | Xid | 1 | 1445 | COMMIT /* xid=17 */ |
| mysql-bin.000001 | 1445 | Query | 1 | 1524 | BEGIN |
| mysql-bin.000001 | 1524 | Query | 1 | 1679 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (5, 'Nick', 22, 'M') |
| mysql-bin.000001 | 1679 | Xid | 1 | 1710 | COMMIT /* xid=18 */ |
| mysql-bin.000001 | 1710 | Query | 1 | 1789 | BEGIN |
| mysql-bin.000001 | 1789 | Query | 1 | 1945 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (6, 'Kathy', 18, 'F') |
| mysql-bin.000001 | 1945 | Xid | 1 | 1976 | COMMIT /* xid=19 */ |
| mysql-bin.000001 | 1976 | Query | 1 | 2055 | BEGIN |
| mysql-bin.000001 | 2055 | Query | 1 | 2211 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (7, 'Steve', 36, 'M') |
| mysql-bin.000001 | 2211 | Xid | 1 | 2242 | COMMIT /* xid=20 */ |
| mysql-bin.000001 | 2242 | Query | 1 | 2321 | BEGIN |
| mysql-bin.000001 | 2321 | Query | 1 | 2476 | use `test`; INSERT INTO test.person (id, first_name, age, gender) VALUES (8, 'Anne', 25, 'F') |
| mysql-bin.000001 | 2476 | Xid | 1 | 2507 | COMMIT /* xid=21 */ |
+------------------+------+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
26 rows in set (0.01 sec)

常用命令

1
2
3
4
show variables like 'binlog_format'
set globle binlog_format='MIXED'
show variables like 'log_bin'
show binary logs

遇到问题:

1
2
3
4
5
6
7
8
9
10
11
[mysql@c738746e9623 support-files]$ ./mysql.server start
Starting MySQL... ERROR! The server quit without updating PID file (/usr/local/mysql/data/c738746e9623.pid).

log_bin=ON
log_bin_basename=/var/lib/mysql/mysql-bin
log_bin_index=/var/lib/mysql/mysql-bin.index
server-id=1

# 修改为
server-id = 1
log-bin=mysql-bin

参考地址:

MySQL锁分类

每次在听别人说锁的时候,是不是会有点儿晕?(一会儿排它锁,一会儿GAP锁…)因为你站在不同的角度来说,它的名字就会不同。根据我们DB的引擎、隔离级别不同,导致的锁的情况也会不同

下面根据几种不同的类型对锁做一个划分:

力度划分:

  • **表级锁:**表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定,开销小,加锁快,粒度大,锁冲突概率大,并发度低,适用于读多写少的情况。

  • **页级锁:**页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁。

  • **行级锁:**行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。Innodb存储引擎,默认选项。

模式划分:

  • **记录锁:**其实很好理解,对表中的记录加锁,叫做记录锁,简称行锁。
  • **GAP锁:**只在RR和Serializable级别下生效.通过gap锁防止其他事务在一定区间插入、删除、修改,来避免幻行问题。
  • **Next-key锁:**是 MySQL 的 InnoDB 存储引擎的一种锁实现,MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。
  • 意向锁:意向锁是一种不与行级锁冲突表级锁,这一点非常重要。意向锁分为两种意向共享锁(intention shared lock, IS)与意向排他锁(intention exclusive lock, IX)。
  • **插入意向锁:**普通的Gap Lock 不允许 在 (上一条记录,本记录) 范围内插入数据,插入意向锁Gap Lock 允许 在 (上一条记录,本记录) 范围内插入数据。

机制划分:

  • **悲观锁:**顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
  • **乐观锁:**顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。

兼容性划分:

  • **共享锁:**多个事务只能读数据不能改数据。
  • **排它锁:**又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

http://static.cyblogs.com/WX20191219-140410@2x.png

隔离级别

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

如何查看一个数据库的隔离级别呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1、查看当前会话
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
这是我本地的mysql数据库,也就是说默认的级别就是:REPEATABLE-READ

2、查看系统当前隔离级别
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set (0.00 sec)

查看数据库死锁日志

1
2
3
4
5
6
7
// innodb_locks记录了所有innodb正在等待的锁,和被等待的锁
select * from information_schema.innodb_locks;

// innodb_lock_waits记录了所有innodb锁的持有和等待关系
select * from information_schema.innodb_lock_waits;

show engine innodb status \G

说明:通过show engine innodb status 查看的日志是最新一次记录死锁的日志,但是查看不到完整的事务的sql,通常显示当前正在等待锁的sql;

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
// 表示事务4641对表`sys`.`new_table`持有了IX锁
TABLE LOCK table `sys`.`new_table` trx id 4641 lock mode IX
// space id=38,space id可以唯一确定一张表,表示了锁所在的表
// page no 3,表示锁所在的页号
// index PRIMARY 表示锁位于名为PRIMARY的索引上
// lock_mode X locks rec but not gap 表示x record lock
// 下方的数据表示了被锁定的索引数据,最上面一行代表索引列的十六进制值,在这里表示的就是id=3的数据
RECORD LOCKS space id 38 page no 3 n bits 80 index PRIMARY of table `sys`.`new_table` trx id 4641 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 8; compact format; info bits 0
0: len 4; hex 00000003; asc ;;
1: len 6; hex 0000000011e9; asc ;;
2: len 7; hex a70000011b0128; asc (;;
3: len 4; hex 8000012c; asc ,;;
4: len 1; hex 63; asc c;;
5: len 4; hex 80000006; asc ;;
6: len 3; hex 636363; asc ccc;;
7: len 2; hex 3333; asc 33;;
// lock_mode X表示的是next-key lock,即当前记录的record lock+前一个间隙的gap lock
// 这个锁在名为idx1的索引上,对应的索引列的值为100(hex 64对应十进制),对应聚簇索引的值为1
RECORD LOCKS space id 38 page no 5 n bits 80 index idx1 of table `sys`.`new_table` trx id 4643 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 00000064; asc d;;
1: len 4; hex 00000001; asc ;;

// lock_mode X locks gap before rec表示的是对应索引记录前一个间隙的gap lock
RECORD LOCKS space id 38 page no 5 n bits 80 index idx1 of table `sys`.`new_table` trx id 4643 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800000c8; asc ;;
1: len 4; hex 00000002; asc ;;

死锁日志解析:

  • lock_mode X locks rec but not gap:模式排它锁,类型行锁;

  • lock_mode X locks gap before rec :模式排它锁,类型gap锁;

  • lock_mode X locks gap before rec insert intention:模式排它锁,类型插入意向锁;

  • lock_mode X:Next-key锁;

总结:

  • 索引记录的间隙上用来避免幻读。

  • Select(Serializable隔离级别除外)不会加锁,而是执行快照读。

  • 写操作都会加锁,具体加锁方式取决于隔离级别、索引命中情况以及修改的索引情况。

  • 为了减少锁的范围,避免死锁的发生,应该尽量让查询条件命中索引,而且命中的越精确加锁越少。同时如果能接受RC级别对一致性的破坏,可以将隔离级别调整成RC。

参考地址

环境准备

前面有几篇文章对于MySQL主从搭建做了一些铺垫:

文章一:MySQL中Binlog的常用设置

文章二:MySQL主从同步-原理&实践篇

先启动Master与Slave的2台mysql服务器,具体信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  ~  docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8f31266d08fc docker-mysql-master:v1 "/usr/sbin/init" 49 minutes ago Up 49 minutes 0.0.0.0:33063->3306/tcp docker-mysql-client
a579aa381425 docker-mysql-slave:v1 "/usr/sbin/init" 19 hours ago Up 19 hours 0.0.0.0:33062->3306/tcp docker-mysql-slave
a40a40c6bde7 docker-mysql-master:v1 "/usr/sbin/init" 19 hours ago Up 19 hours 0.0.0.0:33061->3306/tcp docker-mysql-master

#进入master
➜ ~ docker exec -it 8166c07dd6c7 bash
[root@8166c07dd6c7 /]#

#进入slave
➜ ~ docker exec -it 208c30295ec9 bash
[root@208c30295ec9 /]#

Master机器(172.17.0.2)

1
2
3
4
5
create user 'master_account'@'%' identified by '123456';  
grant replication slave on *.* to 'master_account'@'%';
flush privileges;

change master to master_host='172.17.0.3',master_user='slave_account',master_password='123456',master_log_file='mysql-bin.000001',master_log_pos=120;

Slave机器(172.17.0.3)

1
2
3
4
create user 'slave_account'@'%' identified by '123456';  
grant replication slave on *.* to 'slave_account'@'%';
flush privileges;
change master to master_host='172.17.0.2',master_user='master_account',master_password='123456',master_log_file='mysql-bin.000008',master_log_pos=862;

分别在Master与Slave机器验证,必须是互相同步OK的。

1
2
3
4
#并且保证主从是同步的
mysql> show slave status\G;
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

SQL验证,分别在Master执行脚本需要在Slave上看到数据同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Master
INSERT INTO test.person_01 (id, first_name, age, gender) VALUES (10, 'chenyuan', 20, 'M');
# Slave
INSERT INTO test.person_01 (id, first_name, age, gender) VALUES (11, 'chenyuan11', 20, 'M');
# 2边数据一致就OK
mysql> select * from person_01;
+------+------------+------+--------+
| id | first_name | age | gender |
+------+------------+------+--------+
| 1 | Bob | 25 | M |
| 2 | Jane | 20 | F |
| 3 | Jack | 30 | M |
| 4 | Bill | 32 | M |
| 5 | Nick | 22 | M |
| 6 | Kathy | 18 | F |
| 7 | Steve | 36 | M |
| 8 | Anne | 25 | F |
| 1 | Vernon | 300 | M |
| 10 | chenyuan | 20 | M |
| 11 | chenyuan11 | 20 | M |
+------+------------+------+--------+
11 rows in set (0.00 sec)

安装KeepAlived

安装好gcc,gcc-c++,make

1
2
yum install gcc gcc-c++ autoconf automake
yum install initscripts -y

分别在Maste机器、Slave机器安装好keepalived

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# master
[root@8166c07dd6c7 /]# yum install -y keepalived
...
Complete!
[root@8166c07dd6c7 /]# keepalived -v
Keepalived v1.3.5 (03/19,2017), git commit v1.3.5-6-g6fa32f2
Copyright(C) 2001-2017 Alexandre Cassen, <acassen@gmail.com>

# slave
[root@208c30295ec9 /]# yum install keepalived
...
Complete!
[root@8166c07dd6c7 /]# keepalived -v
Keepalived v1.3.5 (03/19,2017), git commit v1.3.5-6-g6fa32f2
Copyright(C) 2001-2017 Alexandre Cassen, <acassen@gmail.com>

配置KeepAlived

配置Master机器keepalived

新增shutdown.sh脚本,并且赋值可以执行权限

1
chmod 755 shutdown.sh

内容如下:

1
2
#!/bin/bash
pkill keepalived

配置keepalived.conf文件

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
[root@8166c07dd6c7 keepalived]# vi /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
router_id HA_MySQL
}

vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass chenyuan
}
virtual_ipaddress {
172.17.0.4
}
}

virtual_server 172.17.0.99 3306 {
delay_loop 2
lb_algo wrr
lb_kind DR
persistence_timeout 50
protocol TCP

real_server 172.17.0.99 3306 {
weight 3
notify_down /etc/keepalived/bin/shutdown.sh
TCP_CHECK {
connect_timeout 3
nb_get_retry 3
delay_before_retry 3
connect_port 3306
}
}
}

启动好keepalived服务

1
2
3
4
5
6
7
8
[root@a40a40c6bde7 bin]# systemctl start keepalived.service
[root@a40a40c6bde7 bin]# ps aux | grep keepalived
root 494 0.0 0.1 123016 2104 ? Ss 14:59 0:00 keepalived
root 495 0.0 0.3 125268 7164 ? S 14:59 0:00 keepalived
root 496 0.0 0.2 125140 5700 ? S 14:59 0:00 keepalived
root 515 0.0 0.1 12532 2164 pts/1 S+ 14:59 0:00 grep --color=auto keepalived

[root@a40a40c6bde7 bin]# systemctl stop keepalived.service
配置Slave机器keepalived

新增shutdown.sh脚本,并且赋值可以执行权限

1
chmod 755 shutdown.sh

内容如下:

1
2
#!/bin/bash
pkill keepalived

配置keepalived.conf文件

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
! Configuration File for keepalived

global_defs {
router_id HA_MySQL
}

vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 90
advert_int 1
# nopreempt
authentication {
auth_type PASS
auth_pass chenyuan
}
virtual_ipaddress {
172.17.0.99
}
}

virtual_server 172.17.0.99 3306 {
delay_loop 2
lb_algo wrr
lb_kind DR
persistence_timeout 50
protocol TCP

real_server 172.17.0.3 3306 {
weight 3
notify_down /etc/keepalived/bin/shutdown.sh
TCP_CHECK {
connect_timeout 3
nb_get_retry 3
delay_before_retry 3
connect_port 3306
}
}
}

同样也是启动好keepalived服务。

查看虚拟IP是否已经起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@a40a40c6bde7 mysql]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1
link/tunnel6 :: brd ::
21: eth0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 172.17.0.99/32 scope global eth0
valid_lft forever preferred_lft forever
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@a579aa381425 support-files]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1
link/tunnel6 :: brd ::
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

由此可见,现在172.17.0.99/32`是在master节点上。

验证

通过docker-mysql-client机器来登录数据库,下面显示登录成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@8f31266d08fc bin]# ./mysql -h 172.17.0.99 -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 173
Server version: 5.6.45-log MySQL Community Server (GPL)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
1
2
3
4
5
6
7
mysql> show variables like 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 1 |
+---------------+-------+
1 row in set (0.02 sec)

把mysql进程直接杀掉,类似于机器down的情况。然后再次查看server_id,短暂的失去联系,即可很快的恢复。

杀掉Master的进程:

1
2
3
4
5
6
[root@a40a40c6bde7 mysql]# ps aux | grep mysql
root 2559 0.0 0.1 15268 2952 pts/2 S 10:13 0:00 /bin/sh /usr/local/mysql/bin/mysqld_safe --datadir=/usr/local/mysql/data --pid-file=/usr/local/mysql/data/a40a40c6bde7.pid
mysql 2859 0.2 23.0 1686996 471820 pts/2 Sl 10:13 0:01 /usr/local/mysql/bin/mysqld --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data --plugin-dir=/usr/local/mysql/lib/plugin --user=mysql --log-error=a40a40c6bde7.err --pid-file=/usr/local/mysql/data/a40a40c6bde7.pid --socket=/tmp/mysql.sock --port=3306
root 2909 0.0 0.1 12532 2084 pts/2 S+ 10:27 0:00 grep --color=auto mysql
[root@a40a40c6bde7 mysql]# kill -9 2559
[root@a40a40c6bde7 mysql]# kill -9 2859

docker-mysql-client节点上继续查看server_id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show variables like 'server_id';
ERROR 2013 (HY000): Lost connection to MySQL server during query
mysql> show variables like 'server_id';
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 62
Current database: *** NONE ***

mysql> show variables like 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 2 |
+---------------+-------+
1 row in set (0.00 sec)

最后还需要反过来验证一边,就是让Slave机器的mysql服务挂掉,让VIP切换到Master节点去。

遇到问题

不能启动keepalived服务

1
2
3
Failed to get D-Bus connection: Operation not permitted
docker run -itd --name docker-mysql-slave --privileged -v /Users/chenyuan/Data/docker/mysql-data-slave:/usr/local/mysql -v /Users/chenyuan/Tools:/root/Tools -e MYSQL_ROOT_PASSWORD=root -p 33062:3306 docker-mysql-slave:v1 /usr/sbin/init
注意这里的--privileged 与 /usr/sbin/init

通过vip登录报错

1
2
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
FLUSH PRIVILEGES;

参考地址

我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题。包括像Google、微软这些公司,Code Review都是基本要求,代码合并之前必须要有人审查通过才行。

然而对于我观察到的大部分软件开发团队来说,认真做Code Review的很少,有的流于形式,有的可能根本就没有Code Review的环节,代码质量只依赖于事后的测试。也有些团队想做好代码审查,但不知道怎么做比较好。

网上关于如何做Code Review的文章已经有很多了,这里我结合自己的一些经验,也总结整理了一下Code Review的最佳实践,希望能对大家做好Code Review有所帮助。

Code Review有什么好处?

很多团队或个人不做Code Review,根源还是不觉得这是一件有意义的事情,不觉得有什么好处。这个问题要从几个角度来看。

  • 首先是团队知识共享的角度

一个开发团队中,水平有高有低,每个人侧重的领域也有不同。怎么让高水平的帮助新人成长?怎么让大家都对自己侧重领域之外的知识保持了解?怎么能有人离职后其他人能快速接手?这些都是团队管理者关心的问题。

而代码审查,就是一个很好的知识共享的方式。通过代码审查,高手可以直接指出新手代码中的问题,新手可以马上从高手的反馈中学习到好的实践,得到更快的成长;通过代码审查,前端也可以去学习后端的代码,做功能模块A的可以去了解功能模块B的。

可能有些高手觉得给新手代码审查浪费时间,自己也没收获。其实不然,新人成长了,就可以更多的帮高手分担繁重的任务;代码审查中花时间,就少一些帮新人填坑擦屁股的时间;良好的沟通能力、发现问题的能力、帮助其他人成长,都是技术转管理或技术上更上一层楼必不可少的能力,而通过代码审查可以有效的去练习这些方面的能力。

  • 然后是代码质量的角度

现实中的项目总是人手缺进度紧,所以被压缩的往往就是自动化测试和代码审查,结果影响代码质量,欠下技术债务,最后还是要加倍偿还。

也有人寄希望于开发后的人工测试,然而对于代码质量来说,很多问题通过测试是测试不出来的,只能通过代码审查。比如说代码的可读性可维护性,比如代码的结构,比如一些特定条件才触发的死循环、逻辑算法错误,还有一些安全上的漏洞也更容易通过代码审查发现和预防。

也有人觉得自己水平高就不需要代码审查了。对于高手来说,让别人审查自己的代码,可以让其他人学习到好的实践;在让其他人审查的同时,在给别人说明自己代码的时候,也等于自己对自己的代码进行了一次审查。这其实就跟我们上学时做数学题一样,真正能拿高分的往往是那些做完后还会认真检查的。

  • 还有团队规范的角度

每个团队都有自己的代码规范,有自己的基于架构设计的开发规范,然而时间一长,就会发现代码中出现很多不遵守代码规范的情况,有很多绕过架构设计的代码。比如难以理解和不规范的命名,比如三层架构里面UI层绕过业务逻辑层直接调用数据访问层代码。

如果这些违反规范的代码被纠正的晚了,后面再要修改就成本很高了,而且团队的规范也会慢慢的形同虚设。

通过代码审查,就可以及时的去发现和纠正这些问题,保证团队规范的执行。

关于代码审查的好处,还有很多,也不一一列举。还是希望能认识到Code Review和写自动化测试一样,都是属于磨刀不误砍柴工的工作,在上面投入一点点时间,未来会收获代码质量,会节约整体的开发时间。

该怎么做?

现在很多人都已经有意识到Code Review的重要性了,只是苦于不知道如何去实践,不知道怎么样算是好的Code Review实践。

把Code Review作为开发流程的必选项而不是可选项

在很早以前,我就尝试过将代码审查作为代码流程的一部分,但只是一个可选项,没有Code Review也可以把代码合并到master。这样的结果就是想起来才会去做Code Review,去检查的时候已经有了太多的代码变更,审查起来非常困难,另外就算审查出问题,也很难得以修改。

我们现在对代码的审查则是作为开发流程的一个必选项,每次开发新功能或者修复Bug,开一个新的分支,分支要合并到master有两个必要条件:

  • 所有的自动化测试通过
  • 有至少一个人Code Review通过,如果是新手的PR,还必须有资深程序员Code Review通过

img

图片来源:How to Do Code Reviews Like a Human

这样把Code Review作为开发流程的一个必选项后,就很好的保证了代码在合并之前有过Code Review。而且这样合并前要求代码审查的流程,好处也很明显:

  • 由于每一次合并前都要做代码审查,这样一般一次审查的代码量也不会太大,对于审查者来说压力也不会太大
  • 如果在Code Review时发现问题,被审查者希望代码能尽快合并,也会积极的对审查出来的问题进行修改,不至于对审查结果太过抵触

如果你觉得Code Review难以推行,不妨先尝试着把Code Review变成你开发流程的一个必选项。

把Code Review变成一种开发文化而不仅仅是一种制度

把Code Review 作为开发流程的必选项后,不代表Code Review这件事就可以执行的很好,因为Code Review 的执行,很大部分程度上依赖于审查者的认真审查,以及被审查者的积极配合,两者缺一不可!

如果仅仅只是当作一个流程制度,那么就可能会流于形式。最终结果就是看起来有Code Review,但没有人认真审查,随便看下就通过了,或者发现问题也不愿意修改。

真要把Code Review这件事做好,必须让Code Review变成团队的一种文化,开发人员从心底接受这件事,并认真执行这件事。

要形成这样的文化,不那么容易,也没有想象的那么难,比如这些方面可以参考:

  • 首先,得让开发人员认识到Code Review这件事为自己、为团队带来的好处
  • 然后,得要有几个人做好表率作用,榜样的力量很重要
  • 还有,对于管理者来说,你激励什么,往往就会得到什么
  • 最后,像写自动化测试一样,把Code Review要作为开发任务的一部分,给审查者和被审查者都留出专门的时间去做这件事,不能光想着马儿跑得快又舍不得给马儿吃草

如何形成这样的文化,有心的话,还有很多方法可以尝试。只有真正让大家都认同和践行,才可能去做好Code Review这件事。

一些Code Review的经验技巧

在做好Code Review这件事上,还有一些经验技巧可以参考。

选什么工具辅助做CODE REVIEW?

现在很多源代码管理工具都自带Code Review工具,典型的像Github、Gitlab、微软的Azure DevOps,尤其是像Gitlab,还可以自己在本地搭建环境,根据自己的需要灵活配置。

配合什么样的开发流程比较好?

Github Flow这样基于分支开发的流程是特别适合搭配Code Review的。其实不管什么样的开发流程,关键点在于代码合并到master(主干)之前,要先做Code Review。

真遇到紧急情况,来不及代码审查怎么办?

虽然原则上,必须要Code Review才能合并,但有时候确实会存在一些紧急情况,比如说线上故障补丁,而又没有其他人在线,那么这种情况下,最好是在任务管理系统中,创建一个Ticket,用来后续跟踪,确保后续补上Code Review,并对Code Review结果有后续的代码更新。

先设计再编码

有些新人发现自己的代码提交PR(Pull Request)后,会收到一堆的Code Review意见,必须要做大量的改动。这多半是因为在开始做之前,没有做好设计,做出来后才发现问题很多。

建议在做一个新功能之前,写一个简单的设计文档,表达清楚自己的设计思路,找资深的先帮你做一下设计的审查,发现设计上的问题。设计上没问题了,再着手开发,那么到Review的时候,相对问题就会少很多。

代码在提交CODE REVIEW之前,作者要自己先REVIEW和测试一遍

我在做代码审查的时候,有时候会发现一些非常明显的问题,有些甚至自己都没有测试过,就等着别人Code Review和测试帮助发现问题。这种依赖心理无论是对自己还是对团队都是很不负责任的。

一个好的开发人员,代码在提交Code Review之前,肯定是要自己先Review一遍,把该写的自动化测试代码写上,自己把基本的测试用例跑一遍的。

我对于团队提交的PR,有个要求就是要在PR的描述中增加截图或者录屏,就是为了通过截图或者录屏,确保提交PR的人自己是先测试过的。这也是一个有效的辅助手段。

PR要小

在做Code Review的时候,如果有大量的文件修改,那么Review起来是很困难的,但如果PR比较小,相对就比较容易Review,也容易发现代码中可能存在的问题。

所以在提交PR时,PR要小,如果是比较大的改动,那么最好分批提交,以减轻审查者的压力。

img

对评论进行分级

在做Code Review时,需要针对审查出有问题的代码行添加评论,如果只是评论,有时候对于被审查者比较难甄别评论所代表的含义,是不是必须要修改。

建议可以对Review的评论进行分级,不同级别的结果可以打上不同的Tag,比如说:

  • [optional]:在评论前面加上一个[optional]标记,表示这个代码行的问题可改可不改

  • [question]:在评论前面加上一个[question]标记,表示对这个代码行不理解,有问题需要问,被审查者需要针对问题进行回复澄清

类似这样的分级可以帮助被审查者直观了解Review结果,提高Review效率。

评论要友好,避免负面词汇;有说不清楚的问题当面沟通

虽然评论是主要的Code Review沟通方式,但也不要过于依赖,有时候面对面的沟通效率更高,也容易消除误解。

另外文明用语,不要用一些负面的词汇。

总结

Code Review是一种非常好的开发实践,如果你还没开始,不妨逐步实践起来;如果已经做了效果不好,不妨对照一下,看有没有把Code Review作为开发流程的必选项而不是可选项?有没有把Code Review变成一种开发文化而不仅仅是一种制度?

1.字节码

1.1 什么是字节码?

Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示。

图1 Java运行示意图

图1 Java运行示意图

对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

本文重点着眼于字节码增强技术,从字节码开始逐层向上,由JVM字节码操作集合到Java中操作字节码的框架,再到我们熟悉的各类框架原理及应用,也都会一一进行介绍。

1.2 字节码结构

.java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,如下图2的左侧部分:

图2 示例代码(左侧)及对应的字节码(右侧)

图2 示例代码(左侧)及对应的字节码(右侧)

编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。接下来我们将一一介绍这十部分:

图3 JVM规定的字节码结构

图3 JVM规定的字节码结构

(1) 魔数(Magic Number)

所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

(2) 版本号

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

(3) 常量池(Constant Pool)

紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图4所示。

图4 常量池的结构

图4 常量池的结构

  • 常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件中共有35个常量。

图5 前十个字节及含义

图5 前十个字节及含义

  • 常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info(如下图6所示),每种类型的结构都是固定的。

图6 各类型的cp_info

图6 各类型的cp_info

具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

图7 CONSTANT_utf8_info的结构(左)及示例(右)

图7 CONSTANT_utf8_info的结构(左)及示例(右)

其他类型的cp_info结构在本文不再赘述,整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose ByteCodeDemo命令,查看JVM反编译后的完整常量池,如下图8所示。可以看到反编译结果将每一个cp_info结构的类型和值都很明确地呈现了出来。

图8 常量池反编译结果

图8 常量池反编译结果

(4) 访问标志

常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

图9 访问标志

图9 访问标志

(5) 当前类名

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

(6) 父类名称

当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

(7) 接口信息

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

(8) 字段表

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

图10 字段表结构

图10 字段表结构

以图2中字节码的字段表为例,如下图11所示。其中字段的访问标志查图9,0002对应为Private。通过索引下标在图8中常量池分别得到字段名为“a”,描述符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。

图11 字段表示例

图11 字段表示例

(9)方法表

字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

图12 方法表结构

图12 方法表结构

方法的权限修饰符依然可以通过图9的值查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,如图13所示。可以看到属性中包括以下三个部分:

  • “Code区”:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。
  • “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
  • “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

图13 反编译后的方法表

图13 反编译后的方法表

(10)附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

1.3 字节码操作集合

在上图13中,Code区的红色编号0~17,就是.java中的方法源代码编译后让JVM真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可。比如上图中第一个助记符为iconst_2,对应到图2中的字节码为0x05,用处是将int值2压入操作数栈中。以此类推,对0~17的助记符理解后,就是完整的add()方法的实现。

1.4 操作数栈和字节码

JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束,与图13中Code区0~17的指令一一对应:

图14 控制操作数栈示意图

图14 控制操作数栈示意图

1.5 查看字节码工具

如果每次查看反编译后的字节码都使用javap命令的话,好非常繁琐。这里推荐一个Idea插件:jclasslib。使用效果如图15所示,代码编译后在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

图15 jclasslib查看字节码

图15 jclasslib查看字节码

2. 字节码增强

在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。

图16 字节码增强技术

图16 字节码增强技术

2.1 ASM

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理解ASM的处理流程,强烈建议读者先对访问者模式进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

图17 ASM修改字节码

图17 ASM修改字节码

2.1.1 ASM API
2.1.1.1 核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
2.1.1.2 树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

2.1.2 直接利用ASM实现AOP

利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。

1
2
3
4
5
public class Base {
public void process(){
System.out.println("process");
}
}

为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

public class Generator {
public static void main(String[] args) throws Exception {
//读取
ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//输出
File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now generator cc success!!!!!");
}
}

MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

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
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
if (!name.equals("<init>") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}

利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

  • 首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 `` 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。
  • 接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
  • MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
  • 综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

完成这两个visitor类后,运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class文件进行查看,可以看到反编译后的代码已经改变了(如图18左侧所示)。然后写一个测试类MyTest,在其中new Base(),并调用base.process()方法,可以看到下图右侧所示的AOP实现效果:

图18 ASM实现AOP的效果

图18 ASM实现AOP的效果

2.1.3 ASM工具

利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline

安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,如图19所示,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。

图19 ASM Bytecode Outline

图19 ASM Bytecode Outline

2.2 Javassist

ASM是在指令层次上操作字节码的,阅读上文后,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。故除此之外,我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist。

利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

  • CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。
  • ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
  • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

了解这四个类后,我们可以写一个小Demo来展示Javassist简单、快速的特点。我们依然是对Base中的process()方法做增强,在方法调用前后分别输出”start”和”end”,实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.meituan.mtrace.agent.javassist.*;

public class JavassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.javassist.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/Users/zen/projects");
Base h = (Base)c.newInstance();
h.process();
}
}

3. 运行时类的重载

3.1 问题引出

上一章重点介绍了两种不同类型的字节码操作框架,且都利用它们实现了较为粗糙的AOP。其实,为了方便大家理解字节码增强技术,在上文中我们避重就轻将ASM实现AOP的过程分为了两个main方法:第一个是利用MyClassVisitor对已编译好的class文件进行修改,第二个是new对象并调用。这期间并不涉及到JVM运行时对类的重加载,而是在第一个main方法中,通过ASM对已编译类的字节码进行替换,在第二个main方法中,直接使用已替换好的新类信息。另外在Javassist的实现中,我们也只加载了一次Base类,也不涉及到运行时重加载类。

如果我们在一个JVM中,先加载了一个类,然后又对其进行字节码增强并重新加载会发生什么呢?模拟这种情况,只需要我们在上文中Javassist的Demo中main()方法的第一行添加Base b=new Base(),即在增强前就先让JVM加载Base类,然后在执行到c.toClass()方法时会抛出错误,如下图20所示。跟进c.toClass()方法中,我们会发现它是在最后调用了ClassLoader的native方法defineClass()时报错。也就是说,JVM是不允许在运行时动态重载一个类的。

图20 运行时重复load类的错误信息

图20 运行时重复load类的错误信息

显然,如果只能在类加载前对类进行强化,那字节码增强技术的使用场景就变得很窄了。我们期望的效果是:在一个持续运行并已经加载了所有类的JVM中,还能利用字节码增强技术对其中的类行为做替换并重新加载。为了模拟这种情况,我们将Base类做改写,在其中编写main方法,每五秒调用一次process()方法,在process()方法中输出一行“process”。

我们的目的就是,在JVM运行中的时候,将process()方法做替换,在其前后分别打印“start”和“end”。也就是在运行中时,每五秒打印的内容由”process”变为打印”start process end”。那如何解决JVM不允许运行时重加载类信息的问题呢?为了达到这个目的,我们接下来一一来介绍需要借助的Java类库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.lang.management.ManagementFactory;

public class Base {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印当前Pid
System.out.println("pid:"+s);
while (true) {
try {
Thread.sleep(5000L);
} catch (Exception e) {
break;
}
process();
}
}

public static void process() {
System.out.println("process");
}
}
3.2 Instrument

instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现,JVMTI这一部分,我们将在下一小节进行介绍。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

我们定义一个实现了ClassFileTransformer接口的类TestTransformer,依然在其中利用Javassist对Base类中的process()方法进行增强,在前后分别打印“start”和“end”,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.instrument.ClassFileTransformer;

public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("Transforming " + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.jvmti.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

现在有了Transformer,那么它要如何注入到正在运行的JVM呢?还需要定义一个Agent,借助Agent的能力将Instrument注入到JVM中。我们将在下一小节介绍Agent,现在要介绍的是Agent中用到的另一个类Instrumentation。在JDK 1.6之后,Instrumentation可以做启动后的Instrument、本地代码(Native Code)的Instrument,以及动态改变Classpath等等。我们可以向Instrumentation中添加上文中定义的Transformer,并指定要被重加载的类,代码如下所示。这样,当Agent被Attach到一个JVM中时,就会执行类字节码替换并重载入JVM的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.instrument.Instrumentation;

public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
//指定我们自己定义的Transformer,在其中利用Javassist做字节码替换
inst.addTransformer(new TestTransformer(), true);
try {
//重定义类并载入新的字节码
inst.retransformClasses(Base.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
3.3 JVMTI & Agent & Attach API

上一小节中,我们给出了Agent类的代码,追根溯源需要先介绍JPDA(Java Platform Debugger Architecture)。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。正如JDPA名称中的Debugger,JDPA其实是一套用于调试Java程序的标准,任何JDK都必须实现该标准。

JPDA定义了一整套完整的体系,它将调试体系分为三部分,并规定了三者之间的通信接口。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:

图21 JPDA

图21 JPDA

现在回到正题,我们可以借助JVMTI的一部分能力,帮助动态重载类信息。JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

而Agent就是JVMTI的一种实现,Agent有两种启动方式,一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。

Attach API 的作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。在下面,我们将通过Attach API的loadAgent()方法,将打包好的Agent jar包动态Attach到目标JVM上。具体实现起来的步骤如下:

  • 定义Agent,并在其中实现AgentMain方法,如上一小节中定义的代码块7中的TestAgent类;
  • 然后将TestAgent类打成一个包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中将Agent-Class属性指定为TestAgent的全限定名,如下图所示;

图22 Manifest.mf

图22 Manifest.mf

  • 最后利用Attach API,将我们打包好的jar包Attach到指定的JVM pid上,代码如下:
1
2
3
4
5
6
7
8
9
import com.sun.tools.attach.VirtualMachine;

public class Attacher {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
// 传入目标 JVM pid
VirtualMachine vm = VirtualMachine.attach("39333");
vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
}
}
  • 由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目标JVM在运行时会走到TestAgent类中定义的agentmain()方法,而在这个方法中,我们利用Instrumentation,将指定类的字节码通过定义的类转化器TestTransformer做了Base类的字节码替换(通过javassist),并完成了类的重新加载。由此,我们达成了“在JVM运行时,改变类的字节码并重新载入类信息”的目的。

以下为运行时重新载入类的效果:先运行Base中的main()方法,启动一个JVM,可以在控制台看到每隔五秒输出一次”process”。接着执行Attacher中的main()方法,并将上一个JVM的pid传入。此时回到上一个main()方法的控制台,可以看到现在每隔五秒输出”process”前后会分别输出”start”和”end”,也就是说完成了运行时的字节码增强,并重新载入了这个类。

图23 运行时重载入类的效果

图23 运行时重载入类的效果

3.4 使用场景

至此,字节码增强技术的可使用范围就不再局限于JVM加载类前了。通过上述几个类库,我们可以在运行时对JVM中的类进行修改并重载了。通过这种手段,可以做的事情就变得很多了:

  • 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。
  • Mock:测试时候对某些服务做Mock。
  • 性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

4. 总结

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

5. 参考文献

http://static.cyblogs.com/macispopular.jpg

Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里。普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒。那么为什么专业人士也对 Mac 情有独钟呢?从个人使用经验来看我想有下面几个原因:

1、Mac OS X 是基于 Unix 的。这一点太重要了,尤其是对开发人员,至少对于我来说很重要,这意味着Unix 下一堆好用的工具都可以随手捡到。如果你是个 windows 开发人员,我想你会在 windows 上装一套cygwin 环境吧?你不用 flex/yacc,grep,screen,ssh,make?好多 open source 的项目只提供cygwin/gcc/make 的编译环境。Mac 就是基于 BSD Unix 的,所有这些都是 built in 的。

2、开发环境。c/c++/java/perl/python/php/ruby/lisp,各种 shell,应有尽有,直接支持,非常方便。你要在 windows 上开发 C++,要装个 Visual Studio 编译器吧?或者其他的 C++ 编译器;你要开发 Java,你要下载 Java SDK 吧,说不定还要一个 Elipse 或者 Netbean;你要用 Perl,要安装一个 Perl 解释器吧,Active Perl?你要 python/php/ruby,你要安装……?开发程序需要库,图像处理,视频处理,人工智能之类大部分库都是只支持 Unix/Linux 的。Mac 基于 Unix,所以这些通通都和 Mac 能很好和睦相处。

3、编辑器 Vi/Emac。作为 程序员/IT 人员一个好用的编辑器太重要了,因为写程序/改系统配置都需要编辑器。我在 Mac 上差不多1/2的时间是 browser/email,另外1/2时间差不多就是 Vi 了。

4、没有病毒/木马。用了5年多的 Mac 就没看到病毒长成什么样,我还看不到 Mac 上装杀毒软件的需要。

5、不需要维护。Mac 买来就直接用,磁盘碎片整理?不需要。装驱动?Mac 装好了,驱动就好了。重装系统?我5年没有重装过一次(期间换了几次不同的 Mac)。

6、简洁。Mac 上所有的操作都简洁到了极致,尽量避免干扰用户,增加了程序员的生产力。比如切换无线网功能,在 Mac 上切换只需要1次鼠标点击就可以完成,在 windows 上需要点击多次鼠标(包括一些很愚蠢的确认对话框);再比如卸载 USB 盘,Mac 只需要1次鼠标点击,windows 至少需要点击右下角图标、停止设备、确认对话框等多次点击。

7、多窗口切换。这个很方便管理打开的程序/文档。我经常要在多个虚拟窗口切换,比如看浏览网页/邮件一个窗口,写程序/文档一个窗口。

8、程序员文化。国外程序员是以 Unix 为主流成长起来的。这一点和国内不同,中国程序员/开发人员大都是从90年代的 DOS 开始的,随着 Windows 的壮大,成长了一批使用 Microsoft 工具的程序员。这也解释了为什么自从 Mac 切换到 Unix 阵营后,Mac 会发展这么快。基于 Unix 的 Mac 一经推出后,迅速赢得了一大批老 Unix hacker 和新 Web 2.0/Linux hacker 的关注,正是因为这些忠实的 fans 影响了他们的人际网络,圈子,博客,从而影响了整个程序员文化。有点像 Ruby on Rails,开始是一小部分人(精英人士)试用,这些人感觉不错就在博客,研讨会等各种场合鼓吹,从而在 Web 开发领域刮起一阵 Ruby 风。

9、苹果很酷。每台电脑,每个系列都设计完美,从包装盒,宣传册,广告,电源线,电脑内部,电脑外观,电脑软件都精心设计,风格统一。甚至微小到螺丝,看过苹果机箱上的螺丝,机箱里面的数据线吗?那个也是设计。每个 Mac 上都标记着:Designed by Apple in California,而不是 Desgined in USA,苹果就是这么酷,“我们是一家加州公司”。苹果的保密措施可以说做到了极致,产品官方不发售就在市场上看不到踪影。

10、企业家精神。苹果的传奇经历吸引了大批硅谷创业者,Apple/Google/Microsoft/Amazon/eBay/Yahoo 代表了创新,进取的企业家精神。这不是一个大原因,但可以看作是 Mac 在国外,尤其是在美国,尤其是在硅谷,尤其是在大学这么流行的一个小原因吧。据调查2007年美国大学 Mac 市场占有率第一,这些大学精英们毕业以后走上工作岗位,走上社会,再过几年其中一部分走入中层,走进高层,他们会如何影响 Mac 呢?

如果对于类似讨论有兴趣可以看看 VPSee 在 Top Language 讨论组上的回复:[TL] Re: [初级] 为何要选择 Mac?对了,你如果还是对上面那张图片有所怀疑的话,可以看看下面这张图片,来自最近的 TechCrunch Hacker 大会

http://static.cyblogs.com/hackathon.jpg

而最经典的就是下面的评论,可以看好久好久~ 点击阅读原文。

参考地址

背景

迁移了一年,这话真的丝毫没有夸张~

1、从2018年底开始从阿里的HSF迁移到SpringCloud,全面拥抱Spring开源框架;

2、然后就是项目组上海、北京、深圳的项目交接,全部由深圳这边来做业务;

3、后面就是全平台的迁移与整合,包括代码、中间件、数据库、网络等;

4、包括中途发布系统迁移了3~4次,网络从阿里云的经典网络迁移到阿里云的VPC网络;

做这一切都是为了(降本增效):提高我们的对接效率,节约我们的成本,对接的人员更加的专业与熟练。我个人觉得做这些还是挺有意义的,只是要尽快的结束掉。

项目如何迁移?

如何建设一个标准统一平台?
配置中心

错综复杂的业务以及以前一些”坑爹“的特殊处理,你想实现平台大统一。谁都是知道是一件很不容易的事情。你系统的包容能力、可拓展能力真的要很强。那如何去实现这些呢?毫无疑问:一套给力的配置中心是少不了的?

方案一:采用MySQL + Redis的方式

优点:以MySQL关系型数据库做基础,用Redis作为缓存提高吞吐。数据库中的配置保持着简单明了,只存储关键节点,一般情况就是Yes or No,再就是具体的值,每次变更完后立马刷新到缓存即可。

缺点:Redis的方式只能通过客户端的拉取,每次修改都要伴随着一个人工或者手动触发的动作。

方案二:采用Zookeeper + MySQL的方式

优点:利用Zookeeper的方式存储数据,能够保证高可用性以及消息能主动推送的效果,然后用MySQL来做降级。

缺点:类似Zookeeper、Diamond的配置中心,是失去关系型查询的能力以及要处理好代码与配置的更新先后顺序。

http://static.cyblogs.com/WX20200113-171033.png

系统编排

业务、原子能力可编排已经成为搭建中台、平台不可缺少的能力?这是更好的复用底层的最好方式。

方式一:责任链编排

一个比较复杂的业务需要通过好几个系统才能完成。比如:一个还款业务需要贷前、贷中业务的支撑才能保证还款业务的正确执行,起码要经过贷后业务、贷中业务、贷前业务、下游系统等子模块的调用才能把还款计划给冲销掉。

贷前业务:Handler4

贷中业务:Handler2Handler3

贷后业务:Handler1Handler5

下游系统:Handler6

那么该条业务就是根据某个产品独特的业务特性来编排的:Handler1→Handler2→Handler3→Handler4→Handler5→Handler6

http://static.cyblogs.com/WX20200114-091359.png

优点:从配置上能够很清晰的看出一个业务的逻辑,并且非常的灵活。其中router-convert-in、router-convert-out、handler-convert-in、handler-convert-out就是一层代理,可以在入口以及每一个原子服务的前后都能做一些事情。

缺点:太灵活,导致没有标准,什么都能做。配置量巨大,对于后期的维护不太友好。

方式二:配置中心

http://static.cyblogs.com/WX20200114-092931.png

每个系统的边界是非常清晰的,因为都是接口来提现你的能力。每个服务接口都要自己保证自己的完整性与一致性。至于某一个业务具体走了多少个Handler是要看产品的配置。

优点:系统能力清晰,边界清晰,代码可读性强。配置小,基本就是Yes or No,再就是一些具体的变量值。

缺点:不够灵活,每一次的功能叠加与优化,都需要走发布流程才能完成。

格转功能

其实格转这个我们在哪儿都需要,有一段时间在Java的MVC模式下,每一层的数据转换差点就怀疑人生了。其实分层或者叫fullstack的思想都是有它的道理的。每个团队都有他特有的约定或者行业约定,只要大家认可就好,在同一个约定下开发才能你快乐我也快乐。

  • Setter 与 Getter
  • BeanCopy
  • @Annotation
  • 一些自定义格转框架

用格转的时候,应该从方便性、可维护性、性能方面去考虑,就看你侧重与哪一点?

数据迁移
项目前期分析

项目迁移最重要的就是一个风险的评估,需要对之前的系统以及新的系统的差异点要非常的清楚,否则可能会出现盲点造成生产故障。数据迁移一般只包括存量的数量,新增的数据将会在系统层面提现。如果遇到数据量大,而且分库分表逻辑不一致的时候,就会出现很多的问题。

  • 对于没有数据要按照一定的规则生成
  • 缺少对应关系的需要建设映射关系…等
数据量的评估

对于每一张表的一个条数、物理磁盘空间占用等的评估。对于后面迁移的瓶颈在哪儿有一个大概的评估。

表映射关系
  • 部分字段需要特殊转换
  • 多个表写入一张表
  • 一张表写入多张表

这里的细节取决与对于2个系统之间的了解,都要在前期做很细的规划与考量。

迁移框架开发

对于迁移功能或者框架的要求需要做到如下才行:

  • 可回滚:全量回滚,单笔删除
  • 可重试:回滚重试、不回滚重试
  • 可校验:提供校验入口,输出校验结果
  • 时效性:一定的时间内,完成迁移、校验动作
  • 可控制:写入速度控制、可暂停、可强制停止
  • 可视化:进度条、耗时、速率~

其中迁移最重要的就是ReaderWriter,但大部分都是在写这里出现了性能问题。达不到迁移时间上的要求导致失败或者特殊处理。

Reader:

  • 分页查询,尽量能按照固定顺序来读;
  • 建议好索引,避免出现数据库底层的性能问题,提前分析加上好索引;
  • 部分数据可以加载在内存里里面,在内存里进行包装;

Writer:

  • 最好是利用并发来写数据,注意好线程安全;
  • 尽量把不同的表数据打散开来,提高数据库的并发;
  • 能批量操作的就批量操作,不要一条条的操作,减少网络的开销;
  • 注意好事物的回滚操作与日志监控

业务验证

  • 对于迁移的数据可验证,最好能做成自动化的验证功能
  • 对于异常数据把补偿功能提前做好
  • 提前做好回滚的方案

指标&异常考虑

  • 可回滚:全量回滚,单笔删除 – 支持以物理表维度全量回滚
  • 可重试:回滚重试、不回滚重试 – 支持,建议采用回滚重试
  • 可校验:提供校验入口,输出校验结果 – 支持
  • 时效性:一定的时间内,完成迁移、校验动作
  • 可控制:写入速度控制、可暂停、可强制停止 – 通过写线程、部署服务数量来控制速录, 支持暂停服务 TaskScheduler
  • 可视化:进度条、耗时、速率 – 抽样打印插入耗时, 支持打印进度条、打印内存占用率、cpu等

总结

真的没有最厉害的系统,只有最合适的系统。不要过度设计、过度设计、过度设计。每一个设计一定是为了解决某种恶心问题来设计的,它可能解决了最恶心的问题但是也附带了它的问题。首先要去任何架构师的设计,其余的问题就是去耐心解决就好。

lambda表达式实战

从例子引出lambda

传递Runnable创建Thread

  • java8之前
1
2
3
4
5
6
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
// do something
}
});
  • java 8 之后
1
new Thread(()->{});

上边的例子比较简单,但是有两个疑问。什么是Lambda表达式?怎么使用lambda表达式?

什么是Lambda表达式?

从上述例子入手,首先我们知道Lambda一般代表的是一个匿名对象;其次我们点击“->”,IDE会帮助我们进入到符合Lambda规范的函数接口。我们来观察下这个符合规范的类的变化。

1
2
3
4
5
// 省略注释
package java.lang;
public interface Runnable {
public abstract void run();
}
1
2
3
4
5
6
// 省略注释
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

我们发现java8后Runnable接口新增了一个注解@FunctionalInterface。下边我们一起来看下这个注解是什么。

FunctionalInterface
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
/**
* An informative annotation type used to indicate that an interface
* type declaration is intended to be a <i>functional interface</i> as
* defined by the Java Language Specification.
*
* Conceptually, a functional interface has exactly one abstract
* method. Since {@linkplain java.lang.reflect.Method#isDefault()
* default methods} have an implementation, they are not abstract. If
* an interface declares an abstract method overriding one of the
* public methods of {@code java.lang.Object}, that also does
* <em>not</em> count toward the interface's abstract method count
* since any implementation of the interface will have an
* implementation from {@code java.lang.Object} or elsewhere.
*
* <p>Note that instances of functional interfaces can be created with
* lambda expressions, method references, or constructor references.
*
* <p>If a type is annotated with this annotation type, compilers are
* required to generate an error message unless:
*
* <ul>
* <li> The type is an interface type and not an annotation type, enum, or class.
* <li> The annotated type satisfies the requirements of a functional interface.
* </ul>
*
* <p>However, the compiler will treat any interface meeting the
* definition of a functional interface as a functional interface
* regardless of whether or not a {@code FunctionalInterface}
* annotation is present on the interface declaration.
*
* @jls 4.3.2. The Class Object
* @jls 9.8 Functional Interfaces
* @jls 9.4.3 Interface Method Body
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
  • 上边文档的核心意思是:@FunctionInterface注解是为了表明这个类是一个函数式接口。
  • 函数式接口有这样的特点:只有一个抽象方法。java8提供了default方法,以及超类Object中的方法(toString,Equals),这些方法不计算抽象方法数量的统计中。
  • 使用上:函数式接口可以配合lambda表达式方法引用构造引用使用
  • 如果类上标记了这个注解,编译器会在编译期进行检查
  • 最后,即使我们没有标注这个注解,编译器也会将它看待成一个函数式接口

好了,从上边我们知道了lambda的特点,接下来我们来聊下怎么使用?

如何使用Lambda

首先,我们去官网查阅Java8新特性,找到Lambda表达式的说明。我们从这个文档的**“Syntax of Lambda Expressions”**部分入手,大概可以得到如下的结论。

Lambda的组成

Lambda主要由下边几部分组成;参数列表,连接符,主体。

  • 参数列表

    • 圆括号内部,参数以“,”分割开来。如(String a,Object b)。
    • 此外,参数的类型和括号,有些时候是可以省略
  • 箭头记号

    • 通过“->”这种特殊符号形式,连接前后。
  • 主体

    • 可以由单个表达式,或者语句块组成。

    • 单个表达式,如”System.out.println(“xxx”)”

    • 语句块

      • 示例1
      1
      2
      3
      {
      System.out.println("xxx");
      }
      • 示例2
      1
      2
      3
      4
      {
      // do something return some result
      return 100;
      }
Lambda的完整用法示例
无返回值的lambda的用例

目的,将具体业务实现交给调用者处理。

  • 定义一个无返回值,符合FunctionInterface规范的接口对象
1
2
3
interface Print<String>{
void printName(String string);
}
使用示例1

我这里的业务逻辑是根据输入参数,执行日志打印操作。实际业务场景下,可能对应的是发送邮件或者MQ这样的具体操作。

1
2
3
4
5
6
7
8
public class LambdaDemo {
public static void main(String[] args) {
PrintSomeThing(name->System.out.println(name),"Hello baigt");
}
public static void PrintSomeThing(Print<String> str,String name) {
str.printName(name);
}
}
使用示例1 的延伸使用
  • 定义 一个使用类
1
2
3
4
5
6
7
8
9
10
11
class Doctor{
String name;
String interest;
public Doctor(String name, String interest) {
this.name = name;
this.interest = interest;
}
public void printName(Print<String> str) {
str.printName(name);
}
}
  • 具体使用
1
2
Doctor doctor=new Doctor("baigt","java and javascript");
doctor.printName(name->System.out.println(name));
有返回值的lambda的用例

目的,将具体业务实现交给调用者处理,并将结果返回。

  • 定义一个有返回值,符合FunctionInterface规范的接口对象
1
2
3
interface GetSomething<String>{
String getThing();
}
  • 定义一个使用者
1
2
3
4
5
6
7
8
9
10
11
12
class Doctor{
String name;
String interest;
public Doctor(String name, String interest) {
this.name = name;
this.interest = interest;
}

public String getInterest(GetSomething<String> get) {
return get.getThing()+","+name;
}
}
  • 使用示例

我这里的业务逻辑是根据输入参数(隐式interest),计算出一个结果返回出来,并对这个结果执行打印操作。

1
2
Doctor doctor=new Doctor("baigt","java and javascript");
System.out.println(doctor.getInterest(() -> "Hi"));

到此处,我们已经大概明白lambda表达式的基本用法。但是还会有两个疑问?

  • 上边例子我们自定义了几个函数式接口,那么还有其他常用的函数式接口?
  • 函数式接口不仅可以通过lambda表达式使用,还可以通过方法引用和构造引用来使用。那么这种引用又是怎么回事?

常用函数接口

我们选中@FunctionInterface注解类,通过Ide的Find Usages功能,会发现在java.util.function包下java8新增了很多类。这里挑几个基础的(其他的基本是功能上的增强或变种)来说。大致上有这么几种。

  • Consumer
  • Supplier
  • Predicate
  • Function

下边会做一个简单的说明和使用。可能不会细致的去讲每一个Api。旨在让大家快速熟悉使用java8 lambda。

Consumer
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
/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, {@code Consumer} is expected
* to operate via side-effects.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #accept(Object)}.
*
* @param <T> the type of the input to the operation
*
* @since 1.8
*/
@FunctionalInterface
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

首先此接口只有一个抽象方法accept,该方法接收一个入参,不返回结果

定义使用类
1
2
3
public static void doConsumer(Consumer consumer,String input) {
consumer.accept(input);
}
  • 使用示例1

接收 “something input”输入,并执行打印操作

1
2
Consumer consumer = input -> System.out.println(input);
doConsumer(consumer,"something input");
  • 使用示例2

将两个Consumer操作串连起来,andThen的后执行。

1
2
3
4
Consumer consumer = input -> System.out.println(input);
doConsumer(consumer.andThen(input2->{
System.out.println("input2");
}),"something input");
Supplier
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Represents a supplier of results.
*
* <p>There is no requirement that a new or distinct result be returned each
* time the supplier is invoked.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #get()}.
*
* @param <T> the type of results supplied by this supplier
*
* @since 1.8
*/
@FunctionalInterface
public interface Supplier<T> {

/**
* Gets a result.
*
* @return a result
*/
T get();
}

首先此接口只有一个抽象方法get,该方法不接收参数,返回一个T类型的结果

定义使用类
1
2
3
public static <T> T doSupplier(Supplier<T> supplier) {
return supplier.get();
}
  • 使用示例1

不传入参数,生成一个指定类型为String或Integer的对象

1
2
System.out.println(doSupplier(() -> "baigt"));
System.out.println(doSupplier(() -> {return Integer.valueOf("10");}));
Predicate
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
import java.util.Objects;

/**
* Represents a predicate (boolean-valued function) of one argument.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #test(Object)}.
*
* @param <T> the type of the input to the predicate
*
* @since 1.8
*/
@FunctionalInterface
public interface Predicate<T> {

/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);

/**
* Returns a composed predicate that represents a short-circuiting logical
* AND of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code false}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ANDed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* AND of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
}

首先此接口只有一个抽象方法test,该方法接受一个T类型的对象,返回一个boolean类型的结果

定义使用类
1
2
3
public static boolean doPredicate(Predicate<String> predicate,String string) {
return predicate.test(string);
}
  • 使用示例1

根据条件,判断输入对象是否符合过滤规则。

1
2
3
System.out.println(doPredicate(input -> input.length() > 5, "12345"));
System.out.println(doPredicate(((Predicate<String>) (input -> input.length() > 5))
.and(input -> input.equalsIgnoreCase("12345")), "12345"));
Function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
import java.util.Objects;

/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {

/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);

/**
* Returns a composed function that first applies the {@code before}
* function to its input, and then applies this function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*
* @param <V> the type of input to the {@code before} function, and to the
* composed function
* @param before the function to apply before this function is applied
* @return a composed function that first applies the {@code before}
* function and then applies this function
* @throws NullPointerException if before is null
*
* @see #andThen(Function)
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*
* @param <V> the type of output of the {@code after} function, and of the
* composed function
* @param after the function to apply after this function is applied
* @return a composed function that first applies this function and then
* applies the {@code after} function
* @throws NullPointerException if after is null
*
* @see #compose(Function)
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

/**
* Returns a function that always returns its input argument.
*
* @param <T> the type of the input and output objects to the function
* @return a function that always returns its input argument
*/
static <T> Function<T, T> identity() {
return t -> t;
}
}

首先此接口只有一个抽象方法apply,该方法接收一个T类型对象,返回一个R类型的结果。

定义使用类
1
2
3
public static Integer doFunction(Function<String,Integer> function,String input) {
return function.apply(input);
}
  • 使用示例1

接收一个String类型的入参,返回Integer类型的结果。示例中没做具体异常判断。

1
2
3
4
5
6
7
8
9
10
11
12
System.out.println(doFunction(input -> input.length(), "baigt"));
// 上述结果为 5
System.out.println(doFunction(((Function<String, Integer>) (input -> input.length())).compose(input -> String.valueOf(input.length() * 3)), "baigt"));
// 上述结果为 2
System.out.println(doFunction(((Function<String, Integer>) (input -> {
System.out.println("notcompose:"+input);
return Integer.valueOf(input)+1;
})).compose(input -> {
System.out.println("compose:"+input);
return String.valueOf(Integer.valueOf(input)*3);
}), "22"));
// 上述结果为 67

compose是先执行的部分,上述例子中,是根据输入参数进行进一步的加工,再作为输入参数传递给具体调用者。

引用

前边提到了方法引用和构造引用两种,其实构造引用是一种特殊方法引用。具体参照官方文档说明中“Kinds of Method References”部分。

种类 用例
类名::静态方法 String::valueOf
实例对象::实例方法 doctor1::getInterest
类名::实例方法 String::toUpperCase
类名::new (构造引用) String::new
静态引用
  • 使用类
1
2
3
public static String doStaticReference(Function<Integer,String> function, Integer input) {
return function.apply(input);
}
  • 示例
1
doStaticReference(String::valueOf,123456);
实例对象引用实例方法
  • 使用类
1
2
3
4
5
6
7
8
9
10
11
class Doctor{
String name;
String interest;
public Doctor(String name, String interest) {
this.name = name;
this.interest = interest;
}
public String getStringInstance(){
return new String(name);
}
}
  • 示例
1
2
Doctor doctor1=new Doctor("baigt007","java");
Supplier<String> instance = doctor1::getInterest;
类引用实例方法
  • 使用类
1
2
3
public static String doMethodReference(Function<String,String> function, String input) {
return function.apply(input);
}
  • 示例
1
doMethodReference(String::toUpperCase,"baigt");O
构造引用
  • 示例
1
Supplier<String> stringInstance = String::new;

原文地址:https://my.oschina.net/lt0314/blog/3144851

Nginx 是优秀的 HTTP 和反向代理服务器,京东各部门都在广泛使用,但普遍都面临着一些问题:

  1. 配置复杂,专业性强。
  2. 配置文件无法批量修改且配置变更依赖重启操作。
  3. 不同应用依赖不同模块、配置项,管理混乱。
  4. 同一应用的 Nginx 无法批量、快速扩容。

所有问题的根源在于 Nginx 是一个单机系统,虽然模块化、高性能,但在互联网高速发展的今天,像京东这样拥有大规模 Nginx、业务集群的场景下,所有问题都有可能被无限放大,针对这种现状我们设计研发了 JEN(JD EXTENDED NGINX),截止目前 JEN 已覆盖京东金融大部分核心业务,如夺宝吧,卡超市,白条等。

一、整体结构

京东Nginx平台化实践

图 1:JEN 结构图

如上图,运维通过 Web 控制台做相应的配置操作,若是分流、限流等配置,则信息入库等待 Nginx 通过 Restful API 同步规则后开始生效;若是平滑升级、重启等强运维性操作,则 Web 控制台通过控制 Ansible 对 Nginx 进行相应操作。

京东Nginx平台化实践

图 2:Nginx 和 Web 控制台多机房部署图

JEN 特点:

  1. 支持 Nginx 自动发现,分组管理,状态监控。
  2. 统一入口,通过抽象配置,简化操作管控 Nginx 集群生命周期,并支持规则批量配置,操作批量执行。
  3. 扩展了原生 Nginx 的分流、限流功能,支持规则的内存实时同步,无需修改配置文件,更无需重启 Nginx 进程。
1. 基础信息

Web 上所有的展示和操作全部基于对基础信息的计算整合,主要包含两类:

  1. 分组信息(业务线、应用、机房、Nginx IP)
  2. Nginx 属性,例如 upstream 信息,server_name,listen_port 等,主要来源 Nginx 读取 Nginx.conf 内容后的信息上报(心跳)

对于分组信息,JEN 支持以下两种方式填充:

  1. 调用外部服务的 Restful API 导入完整的基础信息。
  2. 对自动发现的 Nginx 做分组的手工编辑。

京东Nginx平台化实践

图 3:各分组间关系图

如上图,分组包括业务线、应用、机房、Nginx 共四层关系,在大规模集群环境下可以通过这种关系并结合 Nginx 属性,支持对所有操作的批量执行,如批量修改配置文件,批量升级重启等,解放生产力。

2. 规则获取

用户在 Web 控制台配置后,在 Nginx 端我们实现了全异步的模块支持定时向 Web 获取属于当前 Nginx 的规则信息,规则存储内存,即时生效,其中:

​ a)规则信息每个进程存储一份,避免进程间资源共享导致锁竞争。

​ b)版本号设计,保证规则和心跳的绝对顺序,不因丢包、延迟等网络因素导致版本错乱,而且在规则未变更时 Nginx 无需频繁解析大量规则信息而消耗 CPU 资源。

3. 安全

JEN 支持三类角色,每种角色支持不同的操作权限(默认是普通用户角色,无写权限),任何角色对 Web 的任何操作都会被记录,并在 Web 提供了入口支持多维度操作日志查询,便于审计

4. 监控

我们实现了更为全面的监控信息采集与展示,包括:

​ a)扩展了 tengine 的主动探测模块,支持上游服务器的平均、当前延时统计。

​ b)通过与 Web 的心跳保持支持 Nginx 存活状态监控。

​ c)支持 TCP 连接信息,in/out 流量,QPS,1xx 到 5xx 回应报文等信息监控。

以上的监控信息支持分组统计(业务线、应用、机房)和大屏展示,便于相关人员(业务,运维)实时监控应用状态。

二、分流

概念:根据请求特征(IP,header 中任意关键字)支持把某些特定请求分流到单个或多个上游服务器中,如下图:

京东Nginx平台化实践

图 4:分流示例图

分流主要适用灰度发布,ab testing 等场景,另外我们也对分流功能做了扩展,支持 Web 控制台一键启停上游服务器,便于当应用服务器需要维护或升级时,用户请求正常访问。

三、限流

京东 618 等大促,货物都提前堆积在购物车,等待零点秒杀,换成工程师的语言来说,就是前一秒的 QPS 很低,但是下一秒 QPS 非常高,流量大意味着机器负载高,若一个应用的一两台机器没有扛住,这样就会导致整个应用集群雪崩。

限流不可盲目,首先需要根据业务特点选择合适的限流算法(漏桶算法、令牌桶算法),其次需要结合历史流量、应用服务能力、营销力度等因素综合评定限流参数,最后决定以何种优雅的方式反馈用户。

Nginx 在实现上通过共享内存共享限流中间信息的方式来达到多进程间的状态统一。在 JEN 设计初衷,原本计划和分流一致,即每个进程存储一份限流规则,限流只在当前进程内限流,但不可避免的会出现如下问题:

  1. 每个进程“你限你的,我限我的”,信息不一致进而导致限流不准确。
  2. 类似用户 ID 的限流,在京东这样拥有庞大日活用户的场景下,每个进程需要开辟足够大的内存才能避免限流算法中对于红黑树节点的频繁置换,这样一来 Nginx 占用内存就会随着进程数成倍扩大。

我们的做法:

  1. 预分配共享内存,Nginx 获取到限流规则时动态适配一块共享内存。
  2. 规则共享,生效后实时同步至所有进程,规则链保证所有旧版本规则只有在当前流量更新之后才会删除,如下图:

京东Nginx平台化实践

图 5:规则链

我们在限流功能上的几点扩展:

  1. 支持错误页定制,除了返回 Nginx 静态页,还支持 302 错误页重定向,根据在 Web 控制台的配置可以重定向到任何外部链接,但 302 重定向存在一个问题:用户浏览器的 URL 和内容都发生了变更,意味着用户需要重新输入 URL 重新请求或者是重复之前的操作步骤,用户体验差可能导致用户放弃此次购买行为而转投它家。在逻辑上我们通过 Nginx 的 subrequest 机制支持返回内容发生变更而 URL 保持不变,这样一来每当用户被限流,只需重新刷新页面即可重复之前的操作步骤。

    京东Nginx平台化实践

    图 6:两种错误页对比

  2. 通过扩展限流算法支持限流后一段时间不可用,例如按 IP 限流且某个 IP 已经触发限流,则支持该 IP 一段时间内不可访问,无需重新通过算法计算。

  3. 同步实现了黑名单、白名单功能,通过白名单避免一些复杂场景下的限流“误杀”(例如 nat 网络下按 ip 限流)。

四、运维特性

运维特性主要指 Nginx 的安装、升级、配置文件修改、启停等操作,运维特性与之前介绍内容的最大区别在于需要重启操作,所以结合第三方工具 Ansible 是比较合适的想法(Ansible 相对于 Puppet 等运维工具,其迁移成本相对较小)。

在实际生产中 Ansible 和 Web 为避免单点需要集群部署,我们的方案是:Web 和 Ansible 在同一 PC 上部署,相关数据改用 DB 存储替代 Ansible 本地文件存储,通过这种简单的改造可以方便 Ansible 和 Web 这组“套件”进行扩容。

京东Nginx平台化实践

图 7:自动化运维操作逻辑图

如上图,用户通过 Web 操作控制 Ansible 对 Nginx 进行升级、重启等操作,Web 是 Nginx 操作的统一入口,这是平台化的重要意义所在,可以放弃 SSH,Shell 甚至是监控系统,开始在 JEN 自给自足了。

通过主动拉取或者是用户在页面导入、手工配置,JEN 会为所有 Nginx 存储配置文件,这样不仅原本因为每个应用都依赖不同的配置项而导致管理混乱的局面得到了改善,而且也可以方便的对配置文件做些扩展,例如历史记录追溯,配置比对,配置复用,操作回滚等。

在页面执行相关操作时,Web 会读取 Ansible 的标准输出并在页面实时展示,为了让使用者以相对友好的方式获知进度我们对 Ansible 做了优化:

  1. 丰富了标准输出的内容,尽量细化到每一个步骤。
  2. 格式化标准输出,便于 Web 获取和展示。

Nginx 在生产环境大规模部署,倘若因为一些原因导致 Nginx 大规模异常,这是我们不希望看到的,所以在可靠性方面,JEN 也提供了多种机制来保证:

  1. 三层错误校验,保证只有在完全正确的情况下才会重启和更新进程,中途发生任何错误不影响线上服务

    a)在 Web 填充表单时做第一层校验。

    b)在目标机器做操作时做第二层检测,例如先执行 Nginx –t 校验。

    c)执行完毕做第三层校验,例如端口是否启动,进程数是否一致等。

  2. 灰度执行

    a)单个 Nginx 依次执行,有任何异常立即中断开始人工介入。

    b)按百分比支持批量执行,例如某个机房的 Nginx 先升级 10%。

五、总结

以上整理了京东在 Nginx 平台化方面的一些实践,JEN 提供了统一入口管控整个 Nginx 生命周期,并支持规则的批量修改即时生效,我们希望这些实践经验能对所有读者产生帮助。

不仅简化了 Dubbo 基于 xml 配置的方式,也提高了日常开发效率,甚至提升了工作幸福感。

为了节省亲爱的读者您的时间,请根据以下2点提示来阅读本文,以提高您的阅读收获效率哦。

  • 如果您只有简单的 Java 基础和 Maven 经验,而不熟悉 Dubbo,本文档将帮助您从零开始使用 Spring Boot 开发 Dubbo 服务,并使用 EDAS 服务注册中心实现服务注册与发现。
  • 如果您熟悉 Dubbo,可以选择性地阅读相关章节。

为什么使用 Spring Boot 开发 Dubbo 应用

Spring Boot 使用极简的一些配置,就能快速搭建一个基于 Spring 的应用,提高的日常的开发效率。因此,如果您使用 Spring Boot 来开发基于 Dubbo 的应用,简化了 Bubbo 基于 xml 配置的方式,提高了日常开发效率,提升了工作幸福感。

为什么使用 EDAS 服务注册中心

EDAS 服务注册中心实现了 Dubbo 所提供的 SPI 标准的注册中心扩展,能够完整地支持 Dubbo 服务注册、路由规则配置规则功能

EDAS 服务注册中心能够完全代替 ZooKeeper 和 Redis,作为您 Dubbo 服务的注册中心。同时,与 ZooKeeper 和 Redis 相比,还具有以下优势:

  • EDAS 服务注册中心为共享组件,节省了您运维、部署 ZooKeeper 等组件的机器成本。
  • EDAS 服务注册中心在通信过程中增加了鉴权加密功能,为您的服务注册链路进行了安全加固。
  • EDAS 服务注册中心与 EDAS 其他组件紧密结合,为您提供一整套的微服务解决方案。

本地开发

准备工作
  • 下载、启动及配置轻量级配置中心。

为了便于本地开发,EDAS 提供了一个包含了 EDAS 服务注册中心基本功能的轻量级配置中心。基于轻量级配置中心开发的应用无需修改任何代码和配置就可以部署到云端的 EDAS 中。

请您参考 配置轻量级配置中心 进行下载、启动及配置。推荐使用最新版本。

  • 下载 Maven 并设置环境变量(本地已安装的可略过)。
创建服务提供者

1、创建一个 Spring Boot 工程,命名为 spring-boot-dubbo-provider。

这里我们以 Spring Boot 2.0.6.RELEASE 为例,在 pom.xml 文件中加入如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.edas</groupId>
<artifactId>edas-dubbo-extension</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

</dependencies>

如果您需要选择使用 Spring Boot 1.x 的版本,请使用 Spring Boot 1.5.x 版本,对应的 com.alibaba.boot:dubbo-spring-boot-starter 版本为 0.1.0。

说明: Spring Boot 1.x 版本的生命周期即将在 2019 年 8 月 结束,推荐使用新版本开发您的应用。

2、开发 Dubbo 服务提供者

2.1、Dubbo 中服务都是以接口的形式提供的。因此需要开发一个接口,例如这里的 IHelloService,接口里有若干个可被调用的方法,例如这里的 SayHello 方法。

1
2
3
4
5
package com.alibaba.edas.boot;

public interface IHelloService {
String sayHello(String str);
}

2.2、在服务提供方,需要实现所有以接口形式暴露的服务接口。例如这里实现 IHelloService 接口的类为 HelloServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
package com.alibaba.edas.boot;

import com.alibaba.dubbo.config.annotation.Service;

@Service
public class HelloServiceImpl implements IHelloService {

public String sayHello(String name) {
return "Hello, " + name + " (from Dubbo with Spring Boot)";
}

}

说明: 这里的 Service 注解式 Dubbo 提供的一个注解类,类的全名称为:com.alibaba.dubbo.config.annotation.Service

2.3、配置 Dubbo 服务。在 application.properties/application.yaml 配置文件中新增以下配置:

1
2
3
4
# Base packages to scan Dubbo Components (e.g @Service , @Reference)
dubbo.scan.basePackages=com.alibaba.edas.boot
dubbo.application.name=dubbo-provider-demo
dubbo.registry.address=edas://127.0.0.1:8080

说明:

  • 以上三个配置没有默认值,必须要给出具体的配置。
  • dubbo.scan.basePackages 的值是开发的代码中含有 com.alibaba.dubbo.config.annotation.Service 和 com.alibaba.dubbo.config.annotation.Reference 注解所在的包。多个包之间用逗号隔开。
  • dubbo.registry.address 的值前缀必须是一个 edas:// 开头,后面的ip地址和端口指的是轻量版配置中心

3、开发并启动 Spring Boot 入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.alibaba.edas.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DubboProvider {

public static void main(String[] args) {

SpringApplication.run(DubboProvider.class, args);
}

}

4、登录轻量版配置中心控制台 http://127.0.0.1:8080,在左侧导航栏中单击服务列表 ,查看提供者列表。可以看到服务提供者里已经包含了 com.alibaba.edas.IHelloService,且可以查询该服务的服务分组和提供者 IP。

创建服务消费者

1、创建一个 Spring Boot 工程,命名为 spring-boot-dubbo-consumer。

这里我们以 Spring Boot 2.0.6.RELEASE 为例,在 pom.xml 文件中加入如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.edas</groupId>
<artifactId>edas-dubbo-extension</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

</dependencies>

如果您需要选择使用 Spring Boot 1.x 的版本,请使用 Spring Boot 1.5.x 版本,对应的 com.alibaba.boot:dubbo-spring-boot-starter 版本为 0.1.0。

说明: Spring Boot 1.x 版本的生命周期即将在 2019 年 8 月 结束,推荐使用新版本开发您的应用。

2、开发 Dubbo 消费者

2.1、在服务消费方,需要引入所有以接口形式暴露的服务接口。例如这里 IHelloService 接口。

1
2
3
4
5
package com.alibaba.edas.boot;

public interface IHelloService {
String sayHello(String str);
}

2.2、Dubbo 服务调用。例如需要在 Controller 中调用一次远程 Dubbo 服务,开发的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.alibaba.edas.boot;
import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoConsumerController {

@Reference
private IHelloService demoService;

@RequestMapping("/sayHello/{name}")
public String sayHello(@PathVariable String name) {
return demoService.sayHello(name);
}
}

说明:这里的 Reference 注解是 com.alibaba.dubbo.config.annotation.Reference 。

2.3、配置 Dubbo 服务。在 application.properties/application.yaml 配置文件中新增以下配置:

1
2
dubbo.application.name=dubbo-consumer-demo
dubbo.registry.address=edas://127.0.0.1:8080

说明:

  • 以上两个配置没有默认值,必须要给出具体的配置。
  • dubbo.registry.address 的值前缀必须是一个 edas:// 开头,后面的ip地址和端口指的是轻量版配置中心

3、开发并启动 Spring Boot 入口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.alibaba.edas.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DubboConsumer {

public static void main(String[] args) {

SpringApplication.run(DubboConsumer.class, args);
}

}

登录轻量版配置中心控制台 http://127.0.0.1:8080,在左侧导航栏中单击 服务列表 ,再在服务列表页面选择 调用者列表 ,可以看到包含了 com.alibaba.edas.IHelloService,且可以查看该服务的服务分组和调用者 IP。

结果验证

  • 本地结果验证

curl http://localhost:17080/sayHello/EDAS

Hello, EDAS (from Dubbo with Spring Boot)

  • EDAS 部署结果验证

curl http://localhost:8080/sayHello/EDAS

Hello, EDAS (from Dubbo with Spring Boot)

0%