Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

日期函数

获得当前日期/时间

Name Synonym Description
CURRENT_DATE, CURRENT_DATE() CURDATE() Return the current date as
'YYYY-MM-DD' / YYYYMMDD
CURRENT_TIME, CURRENT_TIME([fsp]) CURTIME([fsp]) Return the current time as
'hh:mm:ss' / hhmmss
The value is expressed in the session time zone.
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP([fsp])
LOCALTIME, LOCALTIME([fsp])
LOCALTIMESTAMP, LOCALTIMESTAMP([fsp])
NOW([fsp]) Return the current date and time as
'YYYY-MM-DD hh:mm:ss' / YYYYMMDDhhmmss
The value is expressed in the session time zone.
UTC_DATE, UTC_DATE() Return the current UTC date as
'YYYY-MM-DD' / YYYYMMDD
UTC_TIME, UTC_TIME([fsp]) Return the current UTC time as
'hh:mm:ss' / hhmmss
UTC_TIMESTAMP, UTC_TIMESTAMP([fsp]) Return the current UTC date and time as
'YYYY-MM-DD hh:mm:ss' / YYYYMMDDhhmmss

注意,MySQL 时间支持的最高存储精度为微秒:

1 秒(s) =
1,000 毫秒(ms) =
1,000,000 微秒(μs) =
1,000,000,000 纳秒(ns) =
1,000,000,000,000 皮秒(ps) =
1,000,000,000,000,000 飞秒(fs) =
1,000,000,000,000,000,000 仄秒(zs) =
1,000,000,000,000,000,000,000 幺秒(ys) =
1,000,000,000,000,000,000,000,000 渺秒(as)

1 微秒(μs) = 10^-6 秒(0.000,001,百万分之一秒)
1 毫秒(ms) = 10^-3 秒(0.001,千分之一秒)

因此 fsp 参数范围只能为 0 ~ 6:

If the fsp argument is given to specify a fractional seconds precision from 0 to 6, the return value includes a fractional seconds part of that many digits.

例子:

1
2
3
4
5
6
SELECT CURDATE();                       -- 2018-08-08,获取当前年月日
SELECT CURTIME(); -- 22:41:30,获取当前时分秒
SELECT NOW(); -- 2018-08-08 22:20:46,获取当前年月日时分秒
SELECT NOW(3); -- 2018-08-08 22:20:46.166,获取当前年月日时分秒毫秒
SELECT NOW(6); -- 2018-08-08 22:20:46.166123,获取当前年月日时分秒毫秒微秒
SELECT CURRENT_TIMESTAMP; -- 2018-08-08 22:20:46,获取当前年月日时分秒

时区查看/修改

5.1.13 MySQL Server Time Zone Support

查看当前时区

1
2
3
4
5
6
7
-- 结果主要看 system_time_zone
show variables like '%time_zone%';

-- 查询系统时区、会话时区、下一事务时区
SELECT @@GLOBAL.time_zone,
@@SESSION.time_zone,
@@time_zone;;

参考:MySQL 中几个关于时间/时区的变量

修改时区

通过 SQL SET 语法临时修改:

1
2
3
4
5
6
7
8
-- 设置 Global 全局时区,重启后失效
set global time_zone = '+8:00';

-- 设置 Session 会话时区,会话关闭后失效
set time_zone = '+8:00';

-- 设置 下一事务 时区,事务结束后失效
set @@time_zone = '+8:00';

通过修改配置文件,重启后永久生效:

1
2
3
4
$ vim /etc/mysql/my.cnf
default-time_zone = '+8:00'

$ service mysql restart

时区转换 函数

CONVERT_TZ(dt, from_tz, to_tz) 函数用于将 DATETIME 类型转为指定时区,例如:

1
2
3
4
5
6
7
8
9
10
11
-- TIMESTAMP 类型
-- 1218124800,获取当前时间戳
SELECT UNIX_TIMESTAMP();

-- DATETIME 类型
-- 2008-08-07 16:00:00 UTC±00:00
SELECT FROM_UNIXTIME( UNIX_TIMESTAMP() );

-- TIMESTAMP 类型 > DATETIME 类型 > DATETIME 类型
-- 2008-08-08 00:00:00 UTC+08:00
SELECT CONVERT_TZ( FROM_UNIXTIME( UNIX_TIMESTAMP() ), '+00:00', '+08:00' ) AS NOW;
1
2
3
-- DATETIME 类型
-- 2008-08-08 00:00:00 UTC+08:00
SELECT CONVERT_TZ( '2008-08-07 16:00:00', '+00:00', '+08:00' );

TIMESTAMP 类型的时区显示,参考:https://dev.mysql.com/doc/refman/5.7/en/datetime.html

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.) By default, the current time zone for each connection is the server’s time. The time zone can be set on a per-connection basis. As long as the time zone setting remains constant, you get back the same value you store. If you store a TIMESTAMP value, and then change the time zone and retrieve the value, the retrieved value is different from the value you stored. This occurs because the same time zone was not used for conversion in both directions. The current time zone is available as the value of the time_zone system variable. For more information, see Section 5.1.13, “MySQL Server Time Zone Support”.

日期/时间类型转换 函数

TIMESTAMP → xxx

FROM_UNIXTIME(unix_timestamp[,format])

Returns a representation of unix_timestamp as a DATETIME or VARCHAR value. The value returned is expressed using the session time zone. (Clients can set the session time zone as described in Section 5.1.13, “MySQL Server Time Zone Support”.) unix_timestamp is an internal timestamp value representing seconds since '1970-01-01 00:00:00' UTC, such as produced by the UNIX_TIMESTAMP() function.

unix_timestamp

  • When unix_timestamp is an integer, the fractional seconds precision of the DATETIME is 0.
  • When unix_timestamp is a decimal value, the fractional seconds precision of the DATETIME is the same as the precision of the decimal value, up to a maximum of 6.
  • When unix_timestamp is a floating point number, the fractional seconds precision of the datetime is 6.

format

  • If format is omitted, the value returned is a DATETIME.
  • If format is supplied, the value returned is a VARCHAR.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 不指定 format 格式,返回 DATETIME 类型
SELECT FROM_UNIXTIME(1447430881);
-> '2015-11-13 10:08:01'

-- 只支持单位为秒的时间戳,不支持毫秒、微秒,需要先除以其精度转为浮点数
SELECT FROM_UNIXTIME(1447430881123 / 1000);
-> '2015-11-13 16:08:01.1230'

-- 运算后,转为整数类型
SELECT FROM_UNIXTIME(1447430881) + 0;
-> 20151113100801

-- 指定 format 格式,返回 VARCHAR 类型
SELECT FROM_UNIXTIME(1447430881, '%Y %D %M %h:%i:%s %x');
-> '2015 13th November 10:08:01 2015'

fomart 参数参考这里

xxx → TIMESTAMP

UNIX_TIMESTAMP([date])

Return a Unix timestamp.

If no date argument, it returns a Unix timestamp representing seconds since '1970-01-01 00:00:00' UTC.

If with a date argument, it returns the value of the argument as seconds since '1970-01-01 00:00:00' UTC.

The server interprets date as a value in the session time zone and converts it to an internal Unix timestamp value in UTC. (Clients can set the session time zone as described in Section 5.1.13, “MySQL Server Time Zone Support”.)

The date argument may be

  • a DATE, DATETIME, or TIMESTAMP string,
  • or a number in YYMMDD, YYMMDDhhmmss, YYYYMMDD, or YYYYMMDDhhmmss format.

If the argument includes a time part, it may optionally include a fractional seconds part.

The return value is

  • an integer if no argument is given or the argument does not include a fractional seconds part,
  • or DECIMAL if an argument is given that includes a fractional seconds part.

When the date argument is a TIMESTAMP column, UNIX_TIMESTAMP() returns the internal timestamp value directly, with no implicit “string-to-Unix-timestamp” conversion.

The valid range of argument values is the same as for the TIMESTAMP data type: '1970-01-01 00:00:01.000000' UTC to '2038-01-19 03:14:07.999999' UTC. If you pass an out-of-range date to UNIX_TIMESTAMP(), it returns 0.

例子:

1
2
3
4
5
6
7
8
-- 1218124800,获取当前时间戳
SELECT UNIX_TIMESTAMP();
-- 1218124800,将当前时间转换为时间戳,等价于上例
SELECT UNIX_TIMESTAMP(now());
-- 1218153600,即:2008-08-08 00:00:00 UTC
SELECT UNIX_TIMESTAMP('2008-08-08 00:00:00');
-- 1218128400,即:2008-08-07 17:00:00 UTC
SELECT UNIX_TIMESTAMP( CONVERT_TZ( '2008-08-08 00:00:00', '+07:00', '+00:00' ) );

DATETIME → String

Date/Time to Str(日期/时间转换为字符串)函数:

DATE_FORMAT(date, format)TIME_FORMAT(time, format)

例子:

1
2
3
4
select date_format(now(), '%Y-%m-%d');                          -- 2018-08-08
select date_format('2018-08-08 22:23:00', '%W %M %Y'); -- Friday August 2018
select date_format('2018-08-08 22:23:01', '%Y%m%d%H%i%s'); -- 20180808222301
select time_format('22:23:01', '%H.%i.%s'); -- 22.23.01

String → DATETIME

STR_TO_DATE(str, format)

This is the inverse of the DATE_FORMAT() function. It takes a string str and a format string format. STR_TO_DATE() returns a DATETIME value if the format string contains both date and time parts, or a DATE or TIME value if the string contains only date or time parts. If the date, time, or datetime value extracted from str is illegal, STR_TO_DATE() returns NULL and produces a warning.

例子:

1
2
3
4
5
select str_to_date('08/09/2008', '%m/%d/%Y');                   -- 2008-08-09
select str_to_date('08/09/08' , '%m/%d/%y'); -- 2008-08-09
select str_to_date('08.09.2008', '%m.%d.%Y'); -- 2008-08-09
select str_to_date('08:09:30', '%h:%i:%s'); -- 08:09:30
select str_to_date('08.09.2008 08:09:30', '%m.%d.%Y %h:%i:%s'); -- 2008-08-09 08:09:30

format 参数

format 参数如下,这里只列出常用的。更多 format 参数参考:

https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format

format 描述
%Y Year, numeric, four digits
%y Year, numeric (two digits)
%M Month name (January..December)
%m Month, numeric (00..12)
%D Day of the month with English suffix (0th, 1st, 2nd, 3rd, …)
%d Day of the month, numeric (00..31)
%H Hour (00..23)
%h Hour (01..12)
%i Minutes, numeric (00..59)
%s Seconds (00..59)
%f Microseconds (000000..999999)
%T Time, 24-hour (hh:mm:ss)
%r Time, 12-hour (hh:mm:ss followed by AM or PM)

日期/时间计算 函数

为日期增加一个时间间隔:date_add()

为日期减去一个时间间隔:date_sub()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set @dt = now();

select date_add(@dt, interval 1 day); -- add 1 day
select date_add(@dt, interval 1 hour); -- add 1 hour
select date_add(@dt, interval 1 minute); -- ...
select date_add(@dt, interval 1 second);
select date_add(@dt, interval 1 microsecond);
select date_add(@dt, interval 1 week);
select date_add(@dt, interval 1 month);
select date_add(@dt, interval 1 quarter);
select date_add(@dt, interval 1 year);

select date_add(@dt, interval -1 day); -- sub 1 day

SELECT DATE_SUB(@dt, INTERVAL 7 DAY); -- 七天前

日期/时间截取 函数

选取日期时间的各个部分:日期、时间、年、季度、月、日、小时、分钟、秒、微秒

1
2
3
4
5
6
7
8
9
10
11
12
13
set @dt = now();

select date(@dt); -- 2008-09-10
select time(@dt); -- 07:15:30.123456
select year(@dt); -- 2008
select quarter(@dt); -- 3
select month(@dt); -- 9
select week(@dt); -- 36
select day(@dt); -- 10
select hour(@dt); -- 7
select minute(@dt); -- 15
select second(@dt); -- 30
select microsecond(@dt); -- 123456

例子

按年/月/日/时统计订单量

按年统计:DATE_FORMAT(create_time,'%Y')

按月统计:DATE_FORMAT(create_time,'%Y-%m')

按日统计:DATE_FORMAT(create_time,'%Y-%m-%d')

按时统计:DATE_FORMAT(create_time,'%Y-%m-%d %H:00:00')

按日统计订单量,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
select count(*), DATE_FORMAT(create_time,'%Y-%m-%d') AS days 
from t_order
where create_time BETWEEN '2008-9-29' AND '2008-9-30'
group by days;

+----------+------------+
| count(*) | hours |
+----------+------------+
| 150628 | 2008-09-01 |
| 172419 | 2008-09-02 |
| 177021 | 2008-09-03 |
| 178917 | 2008-09-04 |
| 180960 | 2008-09-05 |
| 177626 | 2008-09-06 |
| 177504 | 2008-09-07 |
| 166118 | 2008-09-08 |
| 193006 | 2008-09-09 |
| 204156 | 2008-09-10 |
| 196598 | 2008-09-11 |
| 200184 | 2008-09-12 |
| 159169 | 2008-09-13 |
| 179798 | 2008-09-14 |
| 203586 | 2008-09-15 |
| 217863 | 2008-09-16 |
| 231207 | 2008-09-17 |
| 245960 | 2008-09-18 |
| 226578 | 2008-09-19 |
| 211986 | 2008-09-20 |
| 201396 | 2008-09-21 |
| 183012 | 2008-09-22 |
| 221780 | 2008-09-23 |
| 228094 | 2008-09-24 |
| 220251 | 2008-09-25 |
| 240866 | 2008-09-26 |
| 235670 | 2008-09-27 |
| 244964 | 2008-09-28 |
| 98805 | 2008-09-29 |
+----------+------------+
29 rows in set (5.83 sec)

按 N 分钟统计订单量

做法在于将每行的分钟数 MINUTE(create_time) 除以 10 得到的小数使用 FLOOR 函数向下取整,再乘以 10 得到的就是所属区间。例如:

  • 1 分钟 -> 0
  • 25 分钟 -> 20
  • 59 分钟 -> 50

下例按 10 分钟统计订单量:

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
SELECT COUNT(*), DATE_FORMAT(
CONCAT(
DATE(create_time),
' ',
HOUR(create_time),
':',
FLOOR(MINUTE(create_time) / 10) * 10
), '%Y-%m-%d %H:%i'
) AS hours
FROM t_order
WHERE create_time BETWEEN '2008-9-29' AND '2008-9-30'
GROUP BY hours;

+----------+------------------+
| COUNT(*) | hours |
+----------+------------------+
| 51 | 2008-09-29 00:00 |
| 53 | 2008-09-29 00:10 |
| 43 | 2008-09-29 00:20 |
| 59 | 2008-09-29 00:30 |
| 40 | 2008-09-29 00:40 |
| 27 | 2008-09-29 00:50 |
| 22 | 2008-09-29 01:00 |
| 24 | 2008-09-29 01:10 |
| 15 | 2008-09-29 01:20 |
| 15 | 2008-09-29 01:30 |
| 26 | 2008-09-29 01:40 |
| 18 | 2008-09-29 01:50 |
| 1949 | 2008-09-29 02:00 |

参考

https://dev.mysql.com/doc/refman/5.7/en/functions.html

https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html

https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html

https://dev.mysql.com/doc/refman/5.7/en/time-zone-support.html

原则

MySQL 支持的数据类型非常多,选择正确的数据类型对于获得高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于作出更好的选择。

Smaller is usually better

更小的通常更好:

In general, try to use the smallest data type that can correctly store and represent your data. Smaller data types are usually faster, because they use less space on the disk, in memory, and in the CPU cache. They also generally require fewer CPU cycles to process.

Make sure you don’t underestimate the range of values you need to store, though, because increasing the data type range in multiple places in your schema can be a painful and time-consuming operation. If you’re in doubt as to which is the best data type to use, choose the smallest one that you don’t think you’ll exceed. (If the system is not very busy or doesn’t store much data, or if you’re at an early phase in the design process, you can change it easily later.)

更小的数据类型通常更快,因为占用更少的磁盘、内存和 CPU 缓存,并且处理时需要的 CPU 周期也更少。

Simple is good

简单就好:

Fewer CPU cycles are typically required to process operations on simpler data types. For example, integers are cheaper to compare than characters, because character sets and collations (sorting rules) make character comparisons complicated. Here are two examples: you should store dates and times in MySQL’s builtin types instead of as strings, and you should use integers for IP addresses. We discuss these topics further later.

简单数据类型的操作通常需要更少的 CPU 周期、索引性能更好。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使得字符比整型更复杂。

有几个例子:

  • 使用日期与时间类型,而不是字符串来存储日期和时间,以便排序和格式转换。

  • IPv4 地址:按十进制标记法(32-bit decimal notation)使用 32 位无符号整型类型(int unsigned)进行存储,而不是按点分十进制标记法(Dot-decimal notation)使用字符串类型(varchar)进行存储。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    -- 测试表(注意使用 int UNSIGNED 类型存储(存储范围:0 ~ 2^32-1))
    CREATE TABLE `t_iptable` (
    `ip1` int(32) UNSIGNED NOT NULL COMMENT 'IPv4 地址',
    `ip2` varchar(15) NOT NULL COMMENT 'IPv4 地址'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    -- 测试数据
    INSERT INTO t_iptable(ip1, ip2) VALUES(INET_ATON('0.0.0.0'), '0.0.0.0');
    INSERT INTO t_iptable(ip1, ip2) VALUES(INET_ATON('10.198.1.1'), '10.198.1.1');
    INSERT INTO t_iptable(ip1, ip2) VALUES(INET_ATON('255.255.255.255'), '255.255.255.255');

    -- 测试结果
    -- INET_ATON() Return the numeric value of an IP address
    -- INET_NTOA() Return the IP address from a numeric value
    -- INET6_ATON() Return the numeric value of an IPv6 address
    -- INET6_NTOA() Return the IPv6 address from a numeric value
    SELECT INET_NTOA(ip1), ip2 FROM t_iptable;
    +-----------------+-----------------+
    | INET_NTOA(ip1) | ip2 |
    +-----------------+-----------------+
    | 0.0.0.0 | 0.0.0.0 |
    | 10.198.1.1 | 10.198.1.1 |
    | 255.255.255.255 | 255.255.255.255 |
    +-----------------+-----------------+
  • 散列值:使用定长二进制类型(binary),而不是按十六进制标记法使用字符串类型(char)来存储散列值:https://stackoverflow.com/questions/614476/storing-sha1-hash-values-in-mysql

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    -- 测试表
    CREATE TABLE `t_hash` (
    `hash1` binary(20) NOT NULL COMMENT '散列值',
    `hash2` char(40) NOT NULL COMMENT '散列值'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    -- 测试数据
    INSERT INTO t_hash(hash1, hash2) VALUES(0xE562F69EC36E625116376F376D991E41613E9BF3, 'E562F69EC36E625116376F376D991E41613E9BF3');
    INSERT INTO t_hash(hash1, hash2) VALUES(X'E562F69EC36E625116376F376D991E41613E9BF3', 'E562F69EC36E625116376F376D991E41613E9BF3');
    INSERT INTO t_hash(hash1, hash2) VALUES(UNHEX('E562F69EC36E625116376F376D991E41613E9BF3'), 'E562F69EC36E625116376F376D991E41613E9BF3');

    -- 测试结果
    -- BIT_LENGTH() Return length of a string in bits
    -- LENGTH() Return length of a string in bytes
    SELECT HEX(hash1), BIT_LENGTH(hash1), LENGTH(hash1), hash2, BIT_LENGTH(hash2), LENGTH(hash2) FROM t_hash;
    +------------------------------------------+-------------------+---------------+------------------------------------------+-------------------+---------------+
    | HEX(hash1) | BIT_LENGTH(hash1) | LENGTH(hash1) | hash2 | BIT_LENGTH(hash2) | LENGTH(hash2) |
    +------------------------------------------+-------------------+---------------+------------------------------------------+-------------------+---------------+
    | E562F69EC36E625116376F376D991E41613E9BF3 | 160 | 20 | E562F69EC36E625116376F376D991E41613E9BF3 | 320 | 40 |
    +------------------------------------------+-------------------+---------------+------------------------------------------+-------------------+---------------+

Avoid NULL if possible

避免使用 NULL

A lot of tables include nullable columns even when the application does not need to store NULL (the absence of a value), merely because it’s the default. It’s usually best to specify columns as NOT NULL unless you intend to store NULL in them.

It’s harder for MySQL to optimize queries that refer to nullable columns, because they make indexes, index statistics, and value comparisons more complicated. A nullable column uses more storage space and requires special processing inside MySQL. When a nullable column is indexed, it requires an extra byte per entry and can even cause a fixed-size index (such as an index on a single integer column) to be converted to a variable-sized one in MyISAM.

The performance improvement from changing NULL columns to NOT NULL is usually small, so don’t make it a priority to find and change them on an existing schema unless you know they are causing problems. However, if you’re planning to index columns, avoid making them nullable if possible.

There are exceptions, of course. For example, it’s worth mentioning that InnoDB stores NULL with a single bit, so it can be pretty space-efficient for sparsely populated data. This doesn’t apply to MyISAM, though.

NULL 列使得 MySQL 索引、索引统计和值比较都更复杂。值可为 NULL 的列会使用更多的存储空间(例如当可为 NULL 的列被索引时,每个索引记录需要一个额外的字节),在 MySQL 里也需要特殊处理。如果计划在列上建索引,应该尽量避免。

你应该用 0、一个特殊的值或者一个空串代替 NULL值。

参考:《一千个不用 Null 的理由

常用数据类型

数字类型

比特类型

BIT[(M)] 比特类型,M 为 1~64 bit(s)。

Bit-Value Literals

b'value' 符号可用于指定比特值。value 是一组使用 0 和 1 编写的二进制值。例如 b'111'b'10000000' 分别代表 7128 。详见《Bit-Value Literals》。

如果赋值给小于 M 位长的 BIT(M) 类型列,则该值左侧用零填充。例如,为 BIT(6) 列赋值 b'101' 实际上等于赋值 b'000101'

BIT(1) 常用来表示布尔类型b'0' 表示 falseb'1' 表示 true

1 byte = 8 bit。

整数类型

Data Type Storage Required Data Range(signed Data Range(unsigned Description
TINYINT 8 bits, 1 Byte -2^7 ~ 2^7-1 0 ~ 2^8-1 同义词 BOOLBOOLEAN ,0 为 false,!0 为 true
SMALLINT 16 bits, 2 Bytes -2^15 ~ 2^15-1 0 ~ 2^16-1
MEDIUMINT 24 bits, 3 Bytes -2^23 ~ 2^23-1 0 ~ 2^24-1
INT 32 bits, 4 Bytes -2^31 ~ 2^31-1 0 ~ 2^32-1 同义词 INTEGER
BIGINT 64 bits, 8 Bytes -2^63 ~ 2^63-1 0 ~ 2^64-1 SERIAL 等于 BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE 的别名 ,适用于创建主键

INT[(M)] [UNSIGNED] [ZEROFILL] [AUTO_INCREMENT] 整数类型

  • M 最大显示宽度,例如 INT(11)

    这个属性对于大多数应用都是没有意义的:它不会限制值的合法范围,只是规定了 MySQL 的一些交互工具用来显示字符的个数,最大值为 255,一般配合 ZEROFILL 使用。对于存储和计算来说,INT(1)INT(20) 是相同的,并不会影响该类型的占用字节数。

  • ZEROFILL 填充零:

    顾名思义就是用 “0” 填充的意思,也就是在数字位数不够(< M )的空间用字符 “0” 填满。只在一些交互工具中有效,例如 MyCli。

  • UNSIGNED 无符号:

    整数类型有可选的 UNSIGNED 属性,表示不允许负值,可以使正整数的上限提升一倍:

    公式 例子
    有符号的取值范围 -2^(N-1) ~ 2^(N-1)-1 tinyint -2^7 ~ 2^7-1 (-128 ~ 127)
    无符号的取值范围 0 ~ 2^N-1 tinyint UNSIGNED 0 ~ 2^8-1 (0 ~ 255)

    N 为存储长度的位数(1 Byte = 8 bits)。

  • AUTO_INCREMENT 自动递增:

    在需要产生唯一标识符或顺序值时,可利用此属性,这个属性只用于整数类型。AUTO_INCREMENT 值一般从 1 开始,每行增加 1。 一个表中最多只能有一个 AUTO_INCREMENT 列 。对于任何想要使用 AUTO_INCREMENT 的列,应该定义为 NOT NULL,并定义为 PRIMARY KEY 或定义为 UNIQUE 键。

浮点类型(近似值)

FLOAT[(M,D)] [UNSIGNED] [ZEROFILL] 单精度浮点类型(floating-point number),M 是总位数,D 是小数点后面的位数。如果 MD 省略,值将存储到硬件允许的限制。单精度浮点数精确到约 7 位小数。

DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL] 双精度浮点类型(floating-point number),M 是总位数,D 是小数点后面的位数。如果 MD 省略,值将存储到硬件允许的限制。双精度浮点数精确到小数点后 15 位。

Data Type Storage Required Data Range
FLOAT 32 bits, 4 Bytes -3.402823466E+38 to -1.175494351E-38, 0, and 1.175494351E-38 to 3.402823466E+38
DOUBLE 64 bits, 8 Bytes -1.7976931348623157E+308 to -2.2250738585072014E-308, 0, and2.2250738585072014E-308 to 1.7976931348623157E+308

因为浮点值是近似值而不是作为精确值存储的,比值时可能会导致问题。详见《Problems with Floating-Point Values》。

定点类型(精确值)

DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL] 定点类型(fixed-point number),用于存储高精度数值,如货币数据。

M 是总位数(精度,precision),D 是小数点后的位数(标度,scale)。小数点和(对于负数) - 符号不计入 M。如果 D 为 0,则值不包含小数点或小数部分。如果指定 UNSIGNED,则不允许负值。DECIMAL 的所有基本运算 (+, -, *, /) 都以 65 位数的最大精度完成。

Data Type M 精度范围(总位数) D 标度范围(小数位数) 备注
DECIMAL 0~65,默认 10 0~30,默认 0 同义词 DECNUMERICFIXED

例如 :

1
salary DECIMAL(5,2)

可以存储在 salary 列中的值范围从 -999.99 ~ 999.99。

DECIMAL 以二进制格式存储值,每 4 个字节存 9 个数字。例如,DECIMAL(18,9) 小数点两边各存储 9 个数字,一共使用 9 个字节:小数点前的数字使用 4 个字节,小数点后的数字使用 4 个字节,小数点本身占 1 个字节。详见《Precision Math》。

因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用 DECIMAL —— 例如存储财务数据。但在数据量比较大的时候,可以考虑使用 BIGINT 代替 DECIMAL ,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储的财务数据精确到万分之一分,则可以把所有金额乘以一百万,然后将结果存储在 BIGINT 里,这样可以同时避免浮点存储计算不精确和 DECIMAL 精确计算代价高的问题。

字符串类型

In the following table

  • M represents the declared column length in
    • bytes for binary string types (BINARY(M)VARBINARY(M))
    • characters for nonbinary string types (CHAR(M)VARCHAR(M))
  • L represents the actual length in bytes of a given string value.
Binary Strings (Byte Strings) Nonbinary Strings (Character Strings) Storage Required
Fixed-length types CHAR(M) L = M × w bytes, 0 < M <= 255, where w is the number of bytes required for the maximum-length character in the character set.
Fixed-length types BINARY(M) M bytes, 0 <= M <= 255
Variable-length types VARBINARY(M) VARCHAR(M) L = M × w bytes + 1 bytes if column values require 0 − 255 bytes
L = M × w bytes + 2 bytes if values may require more than 255 bytes
其有效最大字节长度取决于行大小限制(默认 65,535 Bytes,在所有列中共享) ,参考:《表列数量和行数限制》。
Variable-length types TINYBLOB TINYTEXT L + 1 bytes, where L < 2^8 = 256 bytes
Variable-length types BLOB TEXT L + 2 bytes, where L < 2^16 = 64 KB
Variable-length types MEDIUMBLOB MEDIUMTEXT L + 3 bytes, where L < 2^24 = 16 MB
Variable-length types LONGBLOB LONGTEXT L + 4 bytes, where L < 2^32 = 4 GB

variable-length types 变长类型需要额外的 1 到 4 个字节记录长度:

  • 1 Byte = 8 bits 刚好可以记录 0~2^8-1 (255) bytes
  • 2 Bytes = 16 bits 刚好可以记录 0~2^16-1 (65,535) bytes
  • 3 Bytes = 24 bits 刚好可以记录 0~2^24 bytes
  • 4 Bytes = 32 bits 刚好可以记录 0~2^32 bytes
Description
Binary Strings (Byte Strings) They have the binary character set and collation, and comparison and sorting are based on the numeric values of the bytes in the values.
Nonbinary Strings (Character Strings) They have a character set other than binary, and values are sorted and compared based on the collation of the character set.

对于 Nonbinary Strings (Character Strings),ML 换算关系如下:

字符集(Character Sets) M L
latin1 1 character 1 byte
gbk 1 character 2 bytes
utf8 1 character 3 bytes
utf8mb4 1 character 4 bytes

BINARYVARBINARY 类型

https://dev.mysql.com/doc/refman/5.7/en/binary-varbinary.html

The BINARY and VARBINARY types are similar to CHAR and VARCHAR, except that they store binary strings rather than nonbinary strings. That is, they store byte strings rather than character strings. This means they have the binary character set and collation, and comparison and sorting are based on the numeric values of the bytes in the values.

Hexadecimal Literals

https://en.wikipedia.org/wiki/Octal

https://dev.mysql.com/doc/refman/8.0/en/hexadecimal-literals.html

Hexadecimal literal values are written using X'val' or 0xval notation, where val contains hexadecimal digits (0..9, A..F). Lettercase of the digits and of any leading X does not matter. A leading 0x is case-sensitive and cannot be written as 0X.

Legal hexadecimal literals:

1
2
3
4
5
6
X'01AF'
X'01af'
x'01AF'
x'01af'
0x01AF
0x01af

By default, a hexadecimal literal is a binary string, where each pair of hexadecimal digits represents a character:

ASCII

1
2
3
4
5
6
7
8
9
-- BIT_LENGTH()	Return length of a string in bits
-- LENGTH() Return length of a string in bytes
-- CHARSET() Returns the character set of the string argument.
SELECT 0x41, X'41', UNHEX('41'), BIT_LENGTH(0x41), LENGTH(0x41), CHARSET(0x41);
+------+-------+-------------+------------------+--------------+---------------+
| 0x41 | X'41' | UNHEX('41') | BIT_LENGTH(0x41) | LENGTH(0x41) | CHARSET(0x41) |
+------+-------+-------------+------------------+--------------+---------------+
| A | A | A | 8 | 1 | binary |
+------+-------+-------------+------------------+--------------+---------------+

UNHEX(str)

For a string argument str, UNHEX(str) interprets each pair of characters in the argument as a hexadecimal number and converts it to the byte represented by the number. The return value is a binary string.

For information about introducers, see Section 10.3.8, “Character Set Introducers”.

CHARVARCHAR 类型

https://dev.mysql.com/doc/refman/5.7/en/char.html

CHARVARCHAR 这两种类型很相似,但它们被存储和检索的方式不同。区别如下:

Data Type 尾部空格是否保留 描述 适用情况
CHAR(M) 用于存储定长字符串。字符长度不足时会填充尾部空格到指定的长度。 存储很短的字符串,或者所有值都接近同一个长度。或经常变更的数据,定长的 CHAR 类型不容易产生碎片。
VARCHAR(M) 用于存储可变长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间。 字符的最大字节长度比平均长度大很多;列的更新很少,所以碎片不是问题;使用了像 UTF-8 这样复杂的字符集,每个字符都使用不同的字节数进行存储。

例子:下表通过存储各种字符串值到 CHAR(4)VARCHAR(4) 列展示 CHARVARCHAR 之间的差别(假设该列使用单字节字符集,例如 latin1):

CHAR(4) 实际字节长度 VARCHAR(4) 实际字节长度
'' ' ' (四个空格) 4 bytes '' 1 byte
'ab' 'ab ' (两个空格) 4 bytes 'ab' 3 bytes
'abcd' 'abcd' 4 bytes 'abcd' 5 bytes
'abcdefgh' 'abcd' 4 bytes 'abcd' 5 bytes

BLOBTEXT 类型

https://dev.mysql.com/doc/refman/5.7/en/blob.html

与其它类型不同,MySQL 把每个 BLOBTEXT 值当做一个独立的对象处理。存储引擎在存储时通常会做特殊处理。当 BLOBTEXT 值太大时,InnoDB 会使用专门的“外部”存储区域来进行存储,此时每个值在行内需要 1~4 个字节存储一个指针,然后在外部存储区域存储实际的值

MySQL 不能将 BLOBTEXT 列全部长度的字符串进行索引,也不能使用这些索引消除排序,因此可以使用“前缀索引”解决这个问题。

日期与时间类型

Data Type Storage Required before MySQL 5.6.4 Storage Required as of MySQL 5.6.4 0 值 取值范围
YEAR 1 byte, little endian Unchanged 0000 1901 to 2155
DATE 3 bytes, little endian Unchanged 0000-00-00 1000-01-01 to 9999-12-31
TIME[(fsp)] 3 bytes, little endian 3 bytes + fractional-seconds storage, big endian 00:00:00 -838:59:59.000000 to 838:59:59.000000
DATETIME[(fsp)] 8 bytes, little endian 5 bytes + fractional-seconds storage, big endian 0000-00-00 00:00:00 1000-01-01 00:00:00.000000 to 9999-12-31 23:59:59.999999
TIMESTAMP[(fsp)] 4 bytes, little endian 4 bytes + fractional-seconds storage, big endian 0000-00-00 00:00:00 UTC 1970-01-01 00:00:01.000000 UTC to 2038-01-19 03:14:07.999999 UTC

TIMESTAMP[(fsp)]

Section 11.2.1, “Date and Time Data Type Syntax”

A timestamp. The range is '1970-01-01 00:00:01.000000' UTC to '2038-01-19 03:14:07.999999' UTC. TIMESTAMP values are stored as the number of seconds since the epoch ('1970-01-01 00:00:00' UTC). A TIMESTAMP cannot represent the value '1970-01-01 00:00:00' because that is equivalent to 0 seconds from the epoch and the value 0 is reserved for representing '0000-00-00 00:00:00', the “zero” TIMESTAMP value.

An optional fsp value in the range from 0 to 6 may be given to specify fractional seconds precision. A value of 0 signifies that there is no fractional part. If omitted, the default precision is 0.

TIMESTAMP 类型的范围如下:

时间戳 二进制字面量 时间
0 00000000 00000000 00000000 00000000 0000-00-00 00:00:00 UTC
1 00000000 00000000 00000000 00000001 1970-01-01 00:00:01 UTC
2^31-1, 2147483647 01111111 11111111 11111111 11111111 2038-01-19 03:14:07 UTC

TIMESTAMP 类型的时区处理:

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.) By default, the current time zone for each connection is the server’s time. The time zone can be set on a per-connection basis. As long as the time zone setting remains constant, you get back the same value you store. If you store a TIMESTAMP value, and then change the time zone and retrieve the value, the retrieved value is different from the value you stored. This occurs because the same time zone was not used for conversion in both directions. The current time zone is available as the value of the time_zone system variable. For more information, see Section 5.1.13, “MySQL Server Time Zone Support”.

用无符号 intbigint 存储时间戳也是一种解决方案,两种方案对比如下:

TIMESTAMP INTBIGINT
时间范围 存在 2K38 问题 时间范围更大
时区支持 无时区,便于国际化业务 无时区,便于国际化业务
自动初始化和更新 支持 不支持
是否支持使用时间戳整数查询 不支持 支持
DBMS 查询显示 支持用本地时区显示(缺省情况下,每个连接使用服务器时区。也可以为每个连接设置时区) 需通过 FROM_UNIXTIME(unix_timestamp[,format]) 函数转换,否则阅读困难

DATETIME[(fsp)]

Section 11.2.1, “Date and Time Data Type Syntax”

A date and time combination. The supported range is '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.999999'. MySQL displays DATETIME values in YYYY-MM-DD hh:mm:ss[.fraction] format, but permits assignment of values to DATETIME columns using either strings or numbers.

An optional fsp value in the range from 0 to 6 may be given to specify fractional seconds precision. A value of 0 signifies that there is no fractional part. If omitted, the default precision is 0.

DATETIME 类型是一个本地时间,与时区无关

默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2018-01-16 22:37:08”。这是 ANSI 标准定义的日期和时间显示方法。

DATETIME 类型允许使用字符串类型或整数类型进行赋值

1
-- TODO

DATETIME 类型与整型比较时,DATETIME 类型会自动转为整型。利用这个特性可以方便快速比较,例如查询时间范围为 2018-02-15 00:00:00 到 2018-02-16 00:00:00:

1
2
3
select count(1) 
from t_table
where createTime between 20180215 and 20180216;

DATETIME 类型非小数部分的编码如下。参考:Section 10.9, “Date and Time Data Type Representation”

1
2
3
4
5
6
7
8
 1 bit  sign           (1= non-negative, 0= negative)
17 bits year*13+month (year 0-9999, month 0-12)
5 bits day (0-31)
5 bits hour (0-23)
6 bits minute (0-59)
6 bits second (0-59)
---------------------------
40 bits = 5 bytes

存储精度(小数秒)

需要注意的是,MySQL 升级到 5.6 之后对日期与时间类型做过调整,可以精确到微秒并指定其精度(最多 6 位),参考 Changes in MySQL 5.6

incompatible_change_of_date_and_time_type

参考 Date and Time Type Storage Requirements 下表列明了日期与时间类型在 MySQL 5.6.4 前后的变化:

date_and_time_type_storage_requirements

通过分析精确到小数部分的秒(Fractional Seconds Precision)所支持的最大十进制数值,并将其转换为二进制表示,可知为什么精度越高所需的存储空间越多:

Fractional Seconds Precision Maximum Decimal Representation Maximum Binary Representation Storage Required
0 0 0 (0 bit) 0 byte
1, 2 99 0110 0011 (8 bits) 1 byte
3, 4 9,999 0010 0111 0000 1111 (16 bits) 2 bytes
5, 6 999,999 0000 1111 0100 0010 0011 1111 (24 bits) 3 bytes

有关于时间值的内部表示的详细信息,参考 MySQL Internals: Important Algorithms and Structures - Date and Time Data Type Representation

最佳实践

  • MySQL 有多种表示日期的数据类型,比如,当只记录年信息的时候,可以使用 YEAR 类型,而没有必要使用 DATE 类型。

  • 节省存储空间,仅在必要时为 TIME, DATETIME, and TIMESTAMP 指定精度:

    A DATETIME or TIMESTAMP value can include a trailing fractional seconds part in up to microseconds (6 digits) precision. In particular, any fractional part in a value inserted into a DATETIME or TIMESTAMP column is stored rather than discarded. With the fractional part included, the format for these values is 'YYYY-MM-DD hh:mm:ss[.fraction]', the range for DATETIME values is '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.999999', and the range for TIMESTAMP values is '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999'. The fractional part should always be separated from the rest of the time by a decimal point; no other fractional seconds delimiter is recognized. For information about fractional seconds support in MySQL, see Section 11.2.7, “Fractional Seconds in Time Values”.

  • 利用自动初始化和更新功能,为 create_timeupdate_time 字段赋值:

    The TIMESTAMP and DATETIME data types offer automatic initialization and updating to the current date and time. For more information, see Section 11.2.6, “Automatic Initialization and Updating for TIMESTAMP and DATETIME”.

    1
    2
    3
    4
    CREATE TABLE t1 (
    ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );
  • 注意每一个类型都有合法的取值范围,当指定确实不合法的值时系统将 “零” 值插入到数据库中。但要注意开启相关 SQL mode

    Invalid DATE, DATETIME, or TIMESTAMP values are converted to the “zero” value of the appropriate type ('0000-00-00' or '0000-00-00 00:00:00'), if the SQL mode permits this conversion. The precise behavior depends on which if any of strict SQL mode and the NO_ZERO_DATE SQL mode are enabled; see Section 5.1.10, “Server SQL Modes”.

默认值

  • 默认值必须是常量,而不能是一个函数或表达式。举个栗子,这意味着你不能将日期列的默认值设置为诸如 NOW()CURRENT_DATE 之类的函数的值。唯一例外是你可以指定 CURRENT_TIMESTAMPTIMESTAMPDATETIME 列的默认值。
  • 隐式默认值定义如下:
    • 数字类型
      • 对于使用 AUTO_INCREMENT 属性声明的整数类型或浮点类型,默认值为下一个序列值。
      • 否则默认值为 0
    • 字符串类型
      • ENUM 的默认值为第一个枚举值。
      • BLOBTEXT 列无法指定默认值。
      • 其它类型的默认值为空字符串。
    • 日期与时间类型
      • TIMESTAMP
        • 如果系统变量 explicit_defaults_for_timestamp 开启,其默认值为 0 值。
        • 否则表中第一列 TIMESTAMP 的默认值为当前时间。
      • 其它类型的默认值为相应的 0 值。

参考

https://dev.mysql.com/doc/refman/5.7/en/data-types.html

https://dev.mysql.com/doc/refman/5.7/en/literals.html

https://dev.mysql.com/doc/refman/5.7/en/column-count-limit.html

MySQL 数据类型:UNSIGNED 注意事项

MySQL 数据类型:二进制类型

MySQL 5.6 时间数据类型功能获得改进

一只天价股票把纳斯达克系统搞“崩了”!

如果要存ip地址,用什么数据类型比较好?

高安全的生产环境下只能使用命令行操作数据库,下面介绍一些常用命令。

连接 DB

1
2
3
4
5
$ mysql -h192.168.0.221 -P3306 -u账号 -p密码 [db_name]

or better:

$ mycli -h192.168.0.221 -P3306 -u账号 -p密码 [db_name]

查看库

1
2
3
4
5
6
7
8
9
10
11
12
// 查看所有库
$ show databases;

+------------------+
| Database |
|------------------|
| db_name_1 |
| db_name_2 |
+------------------+

// 进入某个库
$ use db_name_1;

查看表

所有表

1
2
3
4
5
6
7
8
$ show tables [from db_name];

+---------------------+
| Tables_in_db_name |
|---------------------|
| table_name_1 |
| table_name_2 |
+---------------------+

所有表状态

显示当前使用或者指定的 DB 中的每个表的信息。

由于字段较多,可用 \G 参数按列显示(行转列),起到显示美化的作用,方便查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ show table status [from db_name] \G;

Name | table_name
Engine | InnoDB
Version | 10
Row_format | Compact
Rows | 59079
Avg_row_length | 133
Data_length | 7880704
Max_data_length | 0
Index_length | 21069824
Data_free | 5242880
Auto_increment | 75437
Create_time | 2017-04-13 20:51:55
Update_time | None
Check_time | None
Collation | utf8_general_ci
Checksum | None
Create_options |
Comment | 测试表

比较重要的字段:

字段 描述
Rows 行的数目。部分存储引擎,如 MyISAM,存储精确的数目。
对于其它存储引擎,比如 InnoDB,是一个大约的值,与实际值相差可达40到50%。在这些情况下,使用 SELECT COUNT(*) 来获得准确的数目。
Avg_row_length 平均的行长度。
Data_length 对于 MyISAMData_length 是数据文件的长度(以字节为单位)。
对于 InnoDBData_length 是聚簇索引 clustered index 大约分配的内存量(以字节为单位)。
Index_length 对于 MyISAMIndex_length 是索引文件的长度(以字节为单位)。
对于 InnoDBIndex_length 是非聚簇索引 non-clustered index 大约分配的内存量(以字节为单位)。
Auto_increment 下一个 AUTO_INCREMENT 值。

表结构

查看列名(三者等价):

1
2
3
$ show columns from table_name [from db_name];
$ show columns from [db_name.]table_name;
$ desc table_name; // 简写形式

索引

1
$ show index from table_name;

建表语句

1
$ show create table table_name;

查看用户权限

显示一个用户的权限,显示结果类似于 GRANT 命令:

1
2
3
4
5
6
7
$ show grants [for user_name@'192.168.0.%'];

+---------------------------------------------------------------------------------------------+
| Grants for user_name@192.168.0.% |
+---------------------------------------------------------------------------------------------+
| GRANT SELECT, INSERT, UPDATE, DELETE ON `db_name`.`table_name` TO 'user_name'@'192.168.0.%' |
+---------------------------------------------------------------------------------------------+

查看系统相关

系统状态

显示一些系统特定资源的信息,例如,正在运行的线程数量。

1
$ show status;

系统变量

显示系统变量的名称和值。

1
$ show variables;

DB 进程

显示系统中正在运行的所有进程,也就是当前正在执行的查询。大多数用户可以查看他们自己的进程,但是如果他们拥有process权限,就可以查看所有人的进程,包括密码。

1
2
3
4
5
6
7
8
// 查看当前 DB 进程
$ show processlist;
$ show full processlist;
+----------+-----------+--------------------+---------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----------+-----------+--------------------+---------+---------+------+-------+------------------+
| 33702451 | user_name | 192.168.0.200:49764 | db_name | Query | 0 | init | show processlist |
+----------+-----------+--------------------+---------+---------+------+-------+------------------+
字段 描述
Id 标识,用途:kill 33702451 杀死指定进程。
User 显示执行 SQL 的用户。
Host 显示这个账号是从哪个 IP 连过来的。
db 显示这个进程目前连接的是哪个数据库 。
command 显示当前连接的执行命令,一般就是休眠( sleep ),查询( query ),连接( connect )。
Time 这个状态持续的时间,单位是秒。
State 显示使用当前连接的 SQL 语句的状态。
Info 显示执行的 SQL 语句。

权限

显示服务器所支持的不同权限。

1
$ show privileges;

存储引擎

1
2
3
4
5
$ show engies; // 显示安装以后可用的存储引擎和默认引擎。 

$ show innodb status; // 显示innoDB存储引擎的状态

$ show logs; // 显示BDB存储引擎的日志

警告与错误

1
2
3
$ show warnings; // 显示最后一个执行的语句所产生的错误、警告和通知 

$ show errors; // 只显示最后一个执行语句所产生的错误

在随手科技这几年,兼任了金融前端团队的负责人,将团队从只有 1 名前端开始,扩展到了 10 多人的前端团队,推动了整个金融前端技术栈的建设及发展,是一段挑战自己未知领域的有趣之旅。

我总结了一下,这几年团队共经历了这样几个阶段:

  • 单块项目(服务端渲染) > 项目分层(前后端分离) > 项目拆分(按业务拆分)
  • 前端模块化 > 组件化 > 工程化
  • 事件驱动 > 数据驱动
  • 后台选型:JSP 服务端渲染 + EasyUI > 前后端分离 + Element
  • 浏览器 > 服务端

下面分几个阶段总结下。

阶段1 服务端渲染

2016 年之前,由于团队和项目规模所限,人员构成以后端开发为主、前端开发为辅(就一个前端开发),只能通过最基础的技术栈,以后端人员最熟悉的技术着手进行业务开发并快速上线,因此技术选型都是偏向服务端的:前端只需按照设计师要求切图并输出静态页面(HTML + CSS),加上一些基础的 ES5 实现所需的动画效果和基础交互效果,后端套成 JSP (或 freemarker velocity thymeleaf)进行服务端渲染。后端开发一般会这样解决问题:

  • 通过 SiteMesh 等框架在 JSP 中将网页内容和页面结构分离,以达到页面结构共享的目的;
  • 通过 tld 文件自定义标签,给 JSP 页面提供一些便捷工具(如货币、时间、字符串格式化);
  • 对于一些复杂的页面交互逻辑,在 JSP 页面上通过 <script> 标签直接引用所需的 JavaScript 文件。

作为后端开发人员会觉得:这么写代码也没什么问题啊,毕竟身边的同事都是这么写的,项目也跑得好好的。但问题在于,后端开发写 JS 都是很业余的,而且随着功能越做越多,业务越做越深,前端脚本开始变得难以扩展与维护:

  • 脚本间依赖关系脆弱,加载顺序需要手工维护,一不小心顺序乱了就 JS 报错;
  • 脚本中潜藏着各种全局变量(函数),导致命名冲突、作用域污染,没有合理的进行前端模块化;
  • 各页面没有主入口脚本,代码不知从何看起……

阶段2 前端模块化

2016 年初,我着手重构前端的第一件事就是将前端模块化。

JavaScript 这门语言(或者说老版本 ES5),最为糟糕的地方就是基于全局变量的编程模型(如何避免使用全局变量?),并且由于不支持“类”与“模块”,使得稍具规模的应用都难以扩展。

一番对比和调研 AMD 和 CMD 规范的相关框架之后,第二阶段决定引入 Require.js(英文中文)这个前端框架。Require.js 以模块化的方式组织前端脚本,通过 AMD 规范定义的两个关键函数 define()require() ,我们可以很轻松的在老版本 ES5 上实现模块化功能,解决模块依赖、全局变量与命名冲突的问题,并提供了统一的脚本主入口。

Require.js 入门教程参考此前博文

阶段3 项目分层(前后端分离)

前端模块化虽然提升了项目的可维护性,但由于此阶段前后端项目仍然强耦合,项目和团队仍存在以下问题:

  • 前端完成的 HTML 页面需交付给后端转换为 JSP 页面,多一道无谓的工序。更重要的是,后续前端任何页面修改,都需要通知后端进行同步修改,操作繁琐且易出错;
  • 由于 JSP 页面由后端编写,后端开发如果觉悟不够或者贪图方便,在 JSP 页面中各种 JavaScript 代码信手拈来、Java 变量和 JS 变量混用,导致前后端难以解耦、代码后续难以维护;
  • 后端开发无法专注于业务开发,大量精力浪费于编写前端样式及脚本,分工不明确、不专业。

更为重要的是,当下前端领域日新月异,ES 新版本、层出不穷的新框架,SPA 单页技术、CSS 预处理语言、前端性能优化、自动化构建…… 受限于项目结构、迫于后端人员能力,新技术无法推广落地,前端人员能力也无法完全施展。

2016 年中,我开始渐进式的推动前后端分离,为了不让步子太大扯着蛋,前端主体技术栈仍采用 HTML + Require.js + Zepto,后台采用 EasyUI,并重点解决下面两类问题:

引入自动化构建工具

传统的前端是无需构建的:前端开发编写的 HTML、JS、CSS 可以直接运行在浏览器中,代码所见即所得。但这种传统方式也带来了以下问题:

  • 无法根据不同环境构建代码,解决各环境间的差异。例如不同环境下资源引用路径是不同的,生产往往会使用 CDN 域名;
  • HTML 页面之间各种代码重复,例如一些全局 rem 设置、全局变量、事件,公共样式、脚本、页面布局,提升了维护成本;
  • JS 脚本没被检查(如静态语法分析),团队协作时代码规范程度无法保证;没有单元测试,潜藏缺陷容易直接流到生产环境;
  • CSS 样式无法扩展、浏览器兼容性问题处理复杂(如需手工添加厂商前缀);
  • 静态资源没被合并、压缩,体积大、数量多,导致用户请求慢;
  • 静态资源没被 hash,带来版本管理和缓存问题,更新困难;
  • 静态资源需手工打包上传,操作繁琐;

为了解决这些问题,这个阶段我引入了自动化构建工具 Gulp.js,一些使用实践请参考此前博文。对于一个前端新手来说,这是一个很大的思维转变,自动化构建极大提升前端项目的工程能力,“构建”阶段能够实现很多之前无法实现的效果。

引入 CSS 预处理语言

前后端分离后,由于引入构建工具,前端开发能够自由发挥的空间更多了,这个阶段我们还引入了 less,一门 CSS 预处理语言,提升了编写前端样式的效率。

API 接口设计

前后端分离的另一个重点,在于数据与页面交互方式的改变——服务端渲染 > 前端渲染。因此定义一套统一的 API 接口规范尤其重要。这个阶段我解决掉的问题:

  • 跨域方案选型:代理、JSONP、CORS,平衡了浏览器兼容性和开发便利性最终采用 CORS 方案;
  • 接口规范:编写后端 API 网关层框架,大一统全公司项目的接口入参、出参规范及处理流程;
  • 文档管理:前期手工编写 Markdown 文档 > 后期使用 SwaggerUI 自动生成文档;
  • 搭建 API Mock Server,前期 gulp-mock-server > 后期 RAP Mock Server ,大大提升前后端并行开发效率。

阶段4 项目拆分

2017 年开始,随着业务做大(新业务越做越多,每周还搞各种运营活动)、人员增多,原来的一两个前端项目已经不能满足快速增长的需求了。这个阶段浮现出来的新问题:

  • 人员多、特性多,由于只有几个前端项目,并行开发时 git 分支难以管理,代码合版时容易发生冲突;
  • 测试环境当时只有两套,测试时容易发生代码被覆盖的问题,特性间不好并行测试;
  • 生产发版风险较大,出问题时只能整体回滚,粒度太大,影响前端项目内的其它正常特性。

为了解决上述问题,2017 年我们按业务、活动两个维度进行了项目分拆:

  • 业务
    • 帐户项目
    • 非标项目
    • 基金项目
    • XX 项目 …
  • 活动
    • 首投活动
    • 邀请活动
    • XX 活动 …

各业务、各项目分而治之,由专门的前端组长统筹、排期、开发、发版,满足各业务的个性化需求及节奏差异。

为了进一步提升开发效率,解决模块及组件的复用问题,这个阶段还:

  • 引入了新版 ES6 + Babel 编译器提升 JavaScript 开发效率;
  • 引入了 MV* 库 Vue.js + 自动化构建工具 Webpack,解决之前的 DOM 节点操作 + 事件驱动机制的开发效率低的问题。
  • 引入了 NPM 包管理器,搭建团队专属的仓库(控件库 + 组件库),提升代码复用性。

阶段5 重回服务端渲染

前端技术近年来日新月异,目前 Node.js 的应用已经铺天盖地,Node.js 中间层的出现改变了前后端的合作模式,各大公司前端都把 Node.js 作为前后端分离的新手段,并且在测试、监控等方面沉淀了大量内容。

2018 年起,前端团队也开始在预研 Node.js 技术、搭建各类基础库并尝试在生产中投入使用。以史为鉴,展望未来,只要我们有不断突破自我的勇气,一定能克服困难,让新技术在公司中落地开花,进一步提升团队的开发效率,为公司创造更大的价值。

待续。

参考

Web前后端分离开发思路

前后端分离后的契约

什么是基于数据驱动的前端框架?

Apache Commons 是一个 Apache 项目,专注于可重用 Java 组件的方方面面。

Apache Commons 项目由三个部分组成:

Apache Commons

其中,Apache Commons Lang 是 Java 开发过程中很常用的一个类库,可以理解为它是对 Java Lang and Util Base Libraries 的增强。

Commons Lang 安装方法:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>x.x.x</version>
</dependency>

Commons Lang 总览:

Commons Lang

Commons Lang 提供了以下 package:

  • org.apache.commons.lang3
  • org.apache.commons.lang3.builder
  • org.apache.commons.lang3.concurrent
  • org.apache.commons.lang3.event
  • org.apache.commons.lang3.exception
  • org.apache.commons.lang3.math
  • org.apache.commons.lang3.mutable
  • org.apache.commons.lang3.reflect
  • org.apache.commons.lang3.text
  • org.apache.commons.lang3.text.translate
  • org.apache.commons.lang3.time
  • org.apache.commons.lang3.tuple
    • PairMutablePairImmutablePair
    • TripleMutableTripleImmutableTriple

下面重点来看下最常用的几个工具:

org.apache.commons.lang3

  • StringUtils
  • ArrayUtils
  • BooleanUtils

StringUtils

StringUtils

判空函数

API:

1
2
3
4
5
6
7
8
9
StringUtils.isEmpty(String str)
StringUtils.isNotEmpty(String str)
StringUtils.isBlank(String str)
StringUtils.isNotBlank(String str)
StringUtils.isAnyBlank(CharSequence… css)
StringUtils.isAnyEmpty(CharSequence… css)
StringUtils.isNoneBlank(CharSequence… css)
StringUtils.isNoneEmpty(CharSequence… css)
StringUtils.isWhitespace(CharSequence cs)

看下 isBlankisEmpty 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
StringUtils.isBlank(null) // true
StringUtils.isEmpty(null) // true

StringUtils.isBlank("") // true
StringUtils.isEmpty("") // true

StringUtils.isBlank(" ") // true
StringUtils.isEmpty(" ") // false

StringUtils.isBlank("\n\t") // true
StringUtils.isEmpty("\n\t") // false

isNotEmpty = !isEmpty, isBlank同理

使用 isAnyBlankisAnyEmpty 进行多维判空:

1
2
3
4
StringUtils.isAnyBlank("", "bar", "foo"); // true
StringUtils.isAnyEmpty(" ", "bar", "foo"); // false

isNoneBlank = !isAnyBlank;isNoneEmpty同理

使用 isWhitespace 判断空白:

1
2
3
StringUtils.isWhitespace(null); // false
StringUtils.isWhitespace(""); // true
StringUtils.isWhitespace(" "); // true

判断是否相等函数

API:

1
2
equals(CharSequence cs1,CharSequence cs2)
equalsIgnoreCase(CharSequence str1, CharSequence str2)

例子:

1
2
3
StringUtils.equals("abc", null)  = false
StringUtils.equals("abc", "abc") = true
StringUtils.equals("abc", "ABC") = false

忽略大小写判断:

1
2
3
StringUtils.equalsIgnoreCase("abc", null)  = false
StringUtils.equalsIgnoreCase("abc", "abc") = true
StringUtils.equalsIgnoreCase("abc", "ABC") = true

是否包含函数

API:

1
2
3
4
5
6
containsOnly(CharSequence cs,char… valid)
containsNone(CharSequence cs,char… searchChars)

startsWith(CharSequence str,CharSequence prefix)
startsWithIgnoreCase(CharSequence str,CharSequence prefix)
startsWithAny(CharSequence string,CharSequence… searchStrings)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//判断字符串中所有字符,是否都是出自参数2中
StringUtils.containsOnly("ab", "") = false
StringUtils.containsOnly("abab", "abc") = true
StringUtils.containsOnly("ab1", "abc") = false
StringUtils.containsOnly("abz", "abc") = false

//判断字符串中所有字符,都不在参数2中。
StringUtils.containsNone("abab", 'xyz') = true
StringUtils.containsNone("ab1", 'xyz') = true
StringUtils.containsNone("abz", 'xyz') = false

//判断字符串是否以第二个参数开始
StringUtils.startsWith("abcdef", "abc") = true
StringUtils.startsWith("ABCDEF", "abc") = false

索引下标函数

API:

1
2
3
4
indexOf(CharSequence seq,CharSequence searchSeq)
indexOf(CharSequence seq,CharSequence searchSeq,int startPos)
indexOfIgnoreCase/lastIndexOfIgnoreCase(CharSequence str,CharSequence searchStr)
lastIndexOf(CharSequence seq,int searchChar)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//返回第二个参数开始出现的索引值
StringUtils.indexOf("aabaabaa", "a") = 0
StringUtils.indexOf("aabaabaa", "b") = 2
StringUtils.indexOf("aabaabaa", "ab") = 1

//从第三个参数索引开始找起,返回第二个参数开始出现的索引值
StringUtils.indexOf("aabaabaa", "a", 0) = 0
StringUtils.indexOf("aabaabaa", "b", 0) = 2
StringUtils.indexOf("aabaabaa", "ab", 0) = 1
StringUtils.indexOf("aabaabaa", "b", 3) = 5
StringUtils.indexOf("aabaabaa", "b", 9) = -1

//返回第二个参数出现的最后一个索引值
StringUtils.lastIndexOf("aabaabaa", 'a') = 7
StringUtils.lastIndexOf("aabaabaa", 'b') = 5

StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8) = 7
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8) = 5
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9) = 5

截取函数

API:

1
2
3
4
5
substring(String str,int start)
substringAfter(String str,String separator)
substringBeforeLast(String str,String separator)
substringAfterLast(String str,String separator)
substringBetween(String str,String tag)

例子:

1
2
3
4
5
6
//start>0表示从左向右, start<0表示从右向左, start=0则从左第一位开始
StringUtils.substring("abcdefg", 0) = "abcdefg"
StringUtils.substring("abcdefg", 2) = "cdefg"
StringUtils.substring("abcdefg", 4) = "efg"
StringUtils.substring("abcdefg", -2) = "fg"
StringUtils.substring("abcdefg", -4) = "defg"

// start>0&&end>0从左开始(包括左)到右结束(不包括右),

//start<0&&end<0从右开始(包括右),再向左数到end结束(包括end)

substring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//从第二个参数字符串开始截取,排除第二个字符串
StringUtils.substringAfter("abc", "a") = "bc"
StringUtils.substringAfter("abcba", "b") = "cba"
StringUtils.substringAfter("abc", "c") = ""

//从最后一个字母出现开始截取
StringUtils.substringBeforeLast("abcba", "b") = "abc"
StringUtils.substringBeforeLast("abc", "c") = "ab"
StringUtils.substringBeforeLast("a", "a") = ""
StringUtils.substringBeforeLast("a", "z") = "a"

StringUtils.substringAfterLast("abc", "a") = "bc"
StringUtils.substringAfterLast("abcba", "b") = "a"
StringUtils.substringAfterLast("abc", "c") = ""

StringUtils.substringBetween("tagabctag", null) = null
StringUtils.substringBetween("tagabctag", "") = ""
StringUtils.substringBetween("tagabctag", "tag") = "abc"

删除函数

API:

1
2
3
4
5
6
7
8
StringUtils.remove(String str, char remove)
StringUtils.remove(String str, String remove)
StringUtils.removeEnd(String str, String remove)
StringUtils.removeEndIgnoreCase(String str, String remove)
StringUtils.removePattern(String source, String regex)
StringUtils.removeStart(String str, String remove)
StringUtils.removeStartIgnoreCase(String str, String remove)
StringUtils.deleteWhitespace(String str)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//删除字符
StringUtils.remove("queued", 'u') = "qeed"

//删除字符串
StringUtils.remove("queued", "ue") = "qd"

//删除结尾匹配的字符串
StringUtils.removeEnd("www.domain.com", ".com") = "www.domain"

//删除结尾匹配的字符串,找都不到返回原字符串
StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"

//忽略大小写的
StringUtils.removeEndIgnoreCase("www.domain.com", ".COM") = "www.domain")

//删除所有空白(好用)
StringUtils.deleteWhitespace("abc") = "abc"
StringUtils.deleteWhitespace(" ab c ") = "abc"

删除空白函数

API:

1
2
3
4
trim(String str)
trimToEmpty(String str)
trimToNull(String str)
deleteWhitespace(String str)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
StringUtils.trim("     ")       = ""
StringUtils.trim("abc") = "abc"
StringUtils.trim(" abc ") = "abc"

StringUtils.trimToNull(" ") = null
StringUtils.trimToNull("abc") = "abc"
StringUtils.trimToNull(" abc ") = "abc"
StringUtils.trimToEmpty(" ") = ""
StringUtils.trimToEmpty("abc") = "abc"
StringUtils.trimToEmpty(" abc ") = "abc"

StringUtils.deleteWhitespace("") = ""
StringUtils.deleteWhitespace("abc") = "abc"
StringUtils.deleteWhitespace(" ab c ") = "abc"

替换函数

API:

1
2
3
4
5
6
7
8
9
replace(String text, String searchString, String replacement)
replace(String text, String searchString, String replacement, int max)
replaceChars(String str, char searchChar, char replaceChar)
replaceChars(String str, String searchChars, String replaceChars)
replaceEach(String text, String[] searchList, String[] replacementList)
replaceEachRepeatedly(String text, String[] searchList, String[] replacementList)
replaceOnce(String text, String searchString, String replacement)
replacePattern(String source, String regex, String replacement)
overlay(String str,String overlay,int start,int end)

replace 例子:

1
2
3
4
5
6
7
8
9
StringUtils.replace("aba", "a", "")    = "b"
StringUtils.replace("aba", "a", "z") = "zbz"

//数字就是替换个数,0代表不替换,1代表从开始数起第一个,-1代表全部替换
StringUtils.replace("abaa", "a", "", -1) = "b"
StringUtils.replace("abaa", "a", "z", 0) = "abaa"
StringUtils.replace("abaa", "a", "z", 1) = "zbaa"
StringUtils.replace("abaa", "a", "z", 2) = "zbza"
StringUtils.replace("abaa", "a", "z", -1) = "zbzz"

replaceEach 是对 replace 的增强版,用于一次性替换多个字符。搜索列表和替换长度必须一致,否则报 IllegalArgumentException 异常:

1
2
StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}) = "dcte"

replaceChars 用于对单个字符逐一替换,其操作如下:

replaceChars

1
2
StringUtils.replaceChars("dabcba", "bcd", "yzx") = "xayzya"
StringUtils.replaceChars("abcba", "bc", "y") = "ayya"

replaceOnce 例子:

1
2
StringUtils.replaceOnce("aba", "a", "")    = "ba"
StringUtils.replaceOnce("aba", "a", "z") = "zba"

overlay 例子:

1
2
3
4
5
6
StringUtils.overlay("abcdef", "zzzz", 2, 4)   = "abzzzzef"
StringUtils.overlay("abcdef", "zzzz", 4, 2) = "abzzzzef"
StringUtils.overlay("abcdef", "zzzz", -1, 4) = "zzzzef"
StringUtils.overlay("abcdef", "zzzz", 2, 8) = "abzzzz"
StringUtils.overlay("abcdef", "zzzz", -2, -3) = "zzzzabcdef"
StringUtils.overlay("abcdef", "zzzz", 8, 10) = "abcdefzzzz"

反转函数

API:

1
2
reverse(String str)
reverseDelimited(String str, char separatorChar)

例子:

1
2
3
StringUtils.reverse("bat") = "tab"
StringUtils.reverseDelimited("a.b.c", 'x') = "a.b.c"
StringUtils.reverseDelimited("a.b.c", ".") = "c.b.a"

分隔函数

API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
split(String str)
split(String str, char separatorChar)
split(String str, String separatorChars)
split(String str, String separatorChars, int max)
splitByCharacterType(String str)
splitByCharacterTypeCamelCase(String str)
splitByWholeSeparator(String str, String separator)
splitByWholeSeparator(String str, String separator, int max)
splitByWholeSeparatorPreserveAllTokens(String str, String separator)
splitByWholeSeparatorPreserveAllTokens(String str, String separator, int max)
splitPreserveAllTokens(String str)
splitPreserveAllTokens(String str, char separatorChar)
splitPreserveAllTokens(String str, String separatorChars)
splitPreserveAllTokens(String str, String separatorChars, int max)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//用空白符做空格
StringUtils.split("abc def") = ["abc", "def"]
StringUtils.split("abc def") = ["abc", "def"]
StringUtils.split("a..b.c", '.') = ["a", "b", "c"]

//用字符分割
StringUtils.split("a:b:c", '.') = ["a:b:c"]

//0 或者负数代表没有限制
StringUtils.split("ab:cd:ef", ":", 0) = ["ab", "cd", "ef"]

//分割字符串 ,可以设定得到数组的长度,限定为2
StringUtils.split("ab:cd:ef", ":", 2) = ["ab", "cd:ef"]

//null也可以作为分隔
StringUtils.splitByWholeSeparator("ab de fg", null) = ["ab", "de", "fg"]
StringUtils.splitByWholeSeparator("ab de fg", null) = ["ab", "de", "fg"]
StringUtils.splitByWholeSeparator("ab:cd:ef", ":") = ["ab", "cd", "ef"]
StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]

//带有限定长度的分隔
StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2) = ["ab", "cd:ef"]

合并函数

API:

1
2
3
join(byte[] array,char separator)
join(Object[] array,char separator)
join(Object[] array,char separator,int startIndex,int endIndex)

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//只有一个参数的join,简单合并在一起
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"

//null的话,就是把字符合并在一起
StringUtils.join(["a", "b", "c"], null) = "abc"

//从index为0到3合并,注意是排除3的
StringUtils.join([null, "", "a"], ',', 0, 3) = ",,a"
StringUtils.join(["a", "b", "c"], "--", 0, 3) = "a--b--c"

//从index为1到3合并,注意是排除3的
StringUtils.join(["a", "b", "c"], "--", 1, 3) = "b--c"
StringUtils.join(["a", "b", "c"], "--", 2, 3) = "c"

大小写转换和判断

API:

1
2
3
4
5
6
7
8
9
10
StringUtils.capitalize(String str)
StringUtils.uncapitalize(String str)
StringUtils.upperCase(String str)
StringUtils.upperCase(String str,Locale locale)
StringUtils.lowerCase(String str)
StringUtils.lowerCase(String str,Locale locale)
StringUtils.swapCase(String str)

StringUtils.isAllUpperCase(CharSequence cs)
StringUtils.isAllLowerCase(CharSequence cs)

大小写转换:

  • capitalize 首字母大写
  • upperCase 全部转化为大写
  • swapCase 大小写互转

大小写判断:

  • isAllUpperCase 是否全部大写
  • isAllLowerCase 是否全部小写

缩短省略函数

API:

1
2
3
abbreviate(String str, int maxWidth)
abbreviate(String str, int offset, int maxWidth)
abbreviateMiddle(String str, String middle, int length)

例子:

1
2
3
4
5
6
7
8
9
StringUtils.abbreviate("abcdefg", 6) = "abc..."
StringUtils.abbreviate("abcdefg", 7) = "abcdefg"
StringUtils.abbreviate("abcdefg", 8) = "abcdefg"
StringUtils.abbreviate("abcdefg", 4) = "a..."
StringUtils.abbreviate("abcdefg", 3) = IllegalArgumentException

StringUtils.abbreviate("abcdefghijklmno", 6, 10) = "...ghij..."

StringUtils.abbreviateMiddle("abcdef", ".", 4) = "ab.f"

字符串的长度小于或等于最大长度,返回该字符串。

运算规律:(substring(str, 0, max-3) + “…”)

如果最大长度小于 4,则抛出异常 IllegalArgumentException

相似度函数

API:

1
difference(String str1,String str2)

例子:

1
2
3
4
5
6
7
8
//在str1中寻找str2中没有的的字符串,并返回     
StringUtils.difference("", "abc") = "abc"
StringUtils.difference("abc", "") = ""
StringUtils.difference("abc", "abc") = ""
StringUtils.difference("abc", "ab") = ""
StringUtils.difference("ab", "abxyz") = "xyz"
StringUtils.difference("abcde", "abxyz") = "xyz"
StringUtils.difference("abcde", "xyz") = "xyz"

difference

BooleanUtils

BooleanUtils

ArrayUtils

添加方法

add(boolean[] array,boolean element)
add(T[] array,int index,T element)
addAll(boolean[] array1,boolean… array2)

1
2
3
4
5
6
7
8
//添加元素到数组中        
ArrayUtils.add([true, false], true) = [true, false, true]

//将元素插入到指定位置的数组中
ArrayUtils.add(["a"], 1, null) = ["a", null]
ArrayUtils.add(["a"], 1, "b") = ["a", "b"]
ArrayUtils.add(["a", "b"], 3, "c") = ["a", "b", "c"]
ArrayUtils.add(["a", "b"], ["c", "d"]) = ["a", "b", "c","d"]

克隆方法

1
ArrayUtils.clone(new int[] { 3, 2, 4 }); = {3,2,4}

包含方法

contains(boolean[] array,boolean valueToFind)

1
2
// 查询某个Object是否在数组中
ArrayUtils.contains(new int[] { 3, 1, 2 }, 1); = true

获取长度方法

getLength(Object array)

1
ArrayUtils.getLength(["a", "b", "c"]) = 3

获取索引方法

indexOf(boolean[] array,boolean valueToFind)
indexOf(boolean[] array,boolean valueToFind,int startIndex)

1
2
3
4
5
6
7
8
9
10
//查询某个Object在数组中的位置,可以指定起始搜索位置,找不到返回-1
//从正序开始搜索,搜到就返回当前的index否则返回-1
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 6); = 2
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 2); = -1

//从逆序开始搜索,搜到就返回当前的index,否则返回-1
ArrayUtils.lastIndexOf(new int[] { 1, 3, 6 }, 6); = 2

//从逆序索引为2开始搜索,,搜到就返回当前的index,否则返回-1
ArrayUtils.lastIndexOf(new Object[]{"33","yy","uu"}, "33",2 ) = 0

判空方法

isEmpty(boolean[] array)等等
isNotEmpty(T[] array)

1
2
3
//判断数组是否为空(null和length=0的时候都为空)
ArrayUtils.isEmpty(new int[0]); = true
ArrayUtils.isEmpty(new Object[] { null }); = false

长度相等判断方法

isSameLength(boolean[] array1,boolean[] array2)

1
2
//判断两个数组的长度是否相等
ArrayUtils.isSameLength(new Integer[] { 1, 3, 5 }, new Long[] { "1", "3", "5"}); = true

空数组转换

nullToEmpty(Object[] array)等等

1
2
3
//讲null转化为相应数组
int [] arr1 = null;
int [] arr2 = ArrayUtils.nullToEmpty(arr1);

删除元素方法

remove(boolean[] array,int index)等等
removeElement(boolean[] array,boolean element)
removeAll(T[] array,int… indices)
removeElements(T[] array,T… values)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//删除指定下标的元素        
ArrayUtils.remove([true, false], 1) = [true]
ArrayUtils.remove([true, true, false], 1) = [true, false]

//删除第一次出现的元素
ArrayUtils.removeElement([true, false], false) = [true]
ArrayUtils.removeElement([true, false, true], true) = [false, true]

//删除所有出现的下标的元素
ArrayUtils.removeAll(["a", "b", "c"], 0, 2) = ["b"]
ArrayUtils.removeAll(["a", "b", "c"], 1, 2) = ["a"]

//删除数组出现的所有元素
ArrayUtils.removeElements(["a", "b"], "a", "c") = ["b"]
ArrayUtils.removeElements(["a", "b", "a"], "a") = ["b", "a"]
ArrayUtils.removeElements(["a", "b", "a"], "a", "a") = ["b"]

反转方法

reverse(boolean[] array)等等
reverse(boolean[] array,int startIndexInclusive,int endIndexExclusive)

1
2
3
4
5
6
7
8
//反转数组
int[] array =new int[] { 1, 2, 5 };
ArrayUtils.reverse(array);// {5,2,1}

//指定范围的反转数组,排除endIndexExclusive的
int[] array =new int[] {1, 2, 5 ,3,4,5,6,7,8};
ArrayUtils.reverse(array,2,5);
System.out.println(ArrayUtils.toString(array)); = {1,2,4,3,5,5,6,7,8}

截取数组

subarray(boolean[] array,int startIndexInclusive,int endIndexExclusive)

1
2
3
4
//起始index为2(即第三个数据)结束index为4的数组
ArrayUtils.subarray(newint[] { 3, 4, 1, 5, 6 }, 2, 4); = {1,5}
//如果endIndex大于数组的长度,则取beginIndex之后的所有数据
ArrayUtils.subarray(newint[] { 3, 4, 1, 5, 6 }, 2, 10); = {1,5,6}

打印数组方法

toString(Object array)
toString(Object array,String stringIfNull)

1
2
3
4
5
//打印数组
ArrayUtils.toString(newint[] { 1, 4, 2, 3 }); = {1,4,2,3}
ArrayUtils.toString(new Integer[] { 1, 4, 2, 3 }); = {1,4,2,3}
//如果为空,返回默认信息
ArrayUtils.toString(null, "I'm nothing!"); = I'm nothing!

参考

https://commons.apache.org/

https://www.tutorialspoint.com/commons_collections/index.htm

https://www.tutorialspoint.com/commons_io/index.htm

RPC 框架对比

市面上的 RPC 框架功能比较:

RPC框架功能比较

协议对比

连接个数 连接方式 传输协议 传输方式 序列化 适用范围 适用场景 参考
dubbo:// 单连接 长连接 TCP NIO 异步传输 Hessian 二进制序列化 传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。 常规远程服务方法调用 dubbo
rmi:// 多连接 短连接 TCP 同步传输 Java 标准二进制序列化 传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。 常规远程服务方法调用,与原生RMI服务互操作
hessian:// 多连接 短连接 HTTP 同步传输 Hessian二进制序列化 传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。 页面传输,文件传输,或与原生hessian服务互操作 hession
http:// 多连接 短连接 HTTP 同步传输 表单序列化 传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。 需同时给应用程序和浏览器 JS 使用的服务。
webservice:// 多连接 短连接 HTTP 同步传输 SOAP 文本序列化 系统集成,跨语言调用 Apache CXF
thrift:// Thrift
memcached://
redis://
rest:// 多连接 可长可短 HTTP 同步传输 JSON/XML JAX-RS

REST 协议小结

根据 dubbox、dubbo REST 官方文档,摘录了使用上的一些注意点:

配置总览

服务提供端

1
<dubbo:protocol name="rest" server="" port="" contextpath="" threads="" iothreads="" keepalive="" accepts="" />
配置项 描述 生效范围
name 启用 REST 协议 all
server REST Server 的实现 all
port 端口号 all
contextpath 应用上下文路径 all
threads 线程池大小 jetty、netty、tomcat
iothreads IO worker线程数 netty
keepalive 是否长连接,默认为 true 长连接 netty、tomcat
accepts 最大的HTTP连接数 tomcat

目前在dubbo中支持5种嵌入式rest server的实现:

1
2
3
4
5
6
7
8
<!-- rest协议默认选用jetty。jetty是非常成熟的java servlet容器,并和dubbo已经有较好的集成(目前5种嵌入式server中只有jetty、tomcat、tjws,与dubbo监控系统等完成了无缝的集成)。 -->
<dubbo:protocol name="rest" server="jetty"/>
<!-- 在嵌入式tomcat上,REST的性能比jetty上要好得多(参见官网的基准测试),建议在需要高性能的场景下采用tomcat。 -->
<dubbo:protocol name="rest" server="tomcat"/>
<dubbo:protocol name="rest" server="netty"/>
<!-- 轻量级嵌入式server,非常方便在集成测试中快速启动使用,当然也可以在负荷不高的生产环境中使用。注意,tjws is now deprecated -->
<dubbo:protocol name="rest" server="tjws"/>
<dubbo:protocol name="rest" server="sunhttp"/>

同时也支持采用外部应用服务器来做rest server的实现:

1
2
<!-- web.xml 参考官网 -->
<dubbo:protocol name="rest" server="servlet"/>

服务消费端

如果REST服务的消费端也是dubbo系统,可以配置每个消费端的超时时间和HTTP连接数,详情参考官方文档。

REST 服务提供端

标准 Java REST API:JAX-RS

  • Dubbox 基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写),提供了接近透明的REST调用支持。由于完全兼容Java标准API,所以为dubbo开发的所有REST服务,未来脱离dubbo或者任何特定的REST底层实现一般也可以正常运行。
  • Dubbo的REST调用和dubbo中其它某些RPC不同的是,需要在服务代码中添加JAX-RS的annotation(以及JAXB、Jackson的annotation),如果你觉得这些annotation一定程度“污染”了你的服务代码,你可以考虑编写额外的Facade和DTO类,在Facade和DTO上添加annotation,而Facade将调用转发给真正的服务实现类。当然事实上,直接在服务代码中添加annotation基本没有任何负面作用,而且这本身是Java EE的标准用法,另外JAX-RS和JAXB的annotation是属于java标准,比我们经常使用的spring、dubbo等等annotation更没有vendor lock-in的问题,所以一般没有必要因此而引入额外对象。
  • JAX-RS与Spring MVC的对比:
    • JAX-RS 相对更适合纯粹的服务化应用,也就是传统Java EE中所说的中间层服务。
    • 在dubbo应用中,我想很多人都比较喜欢直接将一个本地的spring service bean(或者叫manager之类的)完全透明的发布成远程服务,则这里用JAX-RS是更自然更直接的,不必额外的引入MVC概念。
  • 就学习 JAX-RS 来说,一般主要掌握其各种 annotation 的用法即可。参考:

HTTP POST/GET 的实现

  • REST服务中虽然建议使用HTTP协议中四种标准方法POST、DELETE、PUT、GET来分别实现常见的“增删改查”,但实际中,我们一般情况直接用POST来实现“增改”,GET来实现“删查”即可(DELETE和PUT甚至会被一些防火墙阻挡)。

JSON、XML 等多数据格式的支持

  • 在一个REST服务同时对多种数据格式支持的情况下,根据JAX-RS标准,一般是通过HTTP中的MIME header(content-type和accept)来指定当前想用的是哪种格式的数据。
  • 目前业界普遍使用的方式,是使用一个URL后缀(.json和.xml)来指定想用的数据格式。比用HTTP Header更简单直观。Twitter、微博等的REST API都是采用这种方式。

定制序列化

  • Dubbo中的REST实现是用JAXB做XML序列化,用Jackson做JSON序列化,所以在对象上添加JAXB或Jackson的annotation即可以定制映射。更多资料请参考JAXB和Jackson的官方文档。
  • 由于JAX-RS的实现一般都用标准的JAXB(Java API for XML Binding)来序列化和反序列化XML格式数据,所以我们需要为每一个要用XML传输的对象添加一个类级别的JAXB annotation @XmlRootElement,否则序列化将报错。

添加自定义的 Filter、Interceptor 等

  • JAX-RS标准的 FilterInterceptor 可以对请求与响应过程做定制化的拦截处理。

添加自定义的 Exception 处理

  • JAX-RS标准的 ExceptionMapper,可以用来定制特定exception发生后应该返回的HTTP响应。

REST 服务消费端

场景1:非 dubbo 的消费端调用 dubbo 的 REST 服务(non-dubbo > dubbo)

  • 使用标准的JAX-RS Client API或者特定REST实现的Client API来调用REST服务。当然,在java中也可以直接用自己熟悉的比如HttpClient,FastJson,XStream等等各种不同技术来实现REST客户端。

场景2:dubbo 消费端调用 dubbo 的 REST 服务(dubbo > dubbo)

  • dubbo消费端调用dubbo的REST服务,这种场景下必须把JAX-RS的annotation添加到服务接口上,这样在dubbo在消费端才能共享相应的REST配置信息,并据之做远程调用。
  • dubbo的REST支持采用Java标准的bean validation annotation(JSR 303)来做输入校验。为了和其他dubbo远程调用协议保持一致,在rest中作校验的annotation必须放在服务的接口上,这样至少有一个好处是,dubbo的消费端可以共享这个接口的信息,dubbo消费端甚至不需要做远程调用,在本地就可以完成输入校验。

参考

http://dubbo.apache.org/zh-cn/docs/user/references/protocol/rest.html

https://github.com/dangdangdotcom/dubbox

https://dangdangdotcom.github.io/dubbox/rest.html

https://mvnrepository.com/artifact/com.gaosi/dubbox

Dubbox fork from Dubbo,目前只发布了一个版本:2.8.4

有了 HTTP,为什么还要 RPC?

本文主要总结 Dubbo 日常使用时的一些常用配置。

配置之间的关系

配置之间的关系

XML 配置 Java Config 配置 配置 解释
<dubbo:application/> com.alibaba.dubbo.config.ApplicationConfig 应用配置 用于配置当前应用信息,不管该应用是提供者还是消费者
<dubbo:registry/> com.alibaba.dubbo.config.RegistryConfig 注册中心配置 用于配置连接注册中心相关信息
<dubbo:monitor/> com.alibaba.dubbo.config.MonitorConfig 监控中心配置 用于配置连接监控中心相关信息,可选
<dubbo:protocol/> com.alibaba.dubbo.config.ProtocolConfig 协议配置 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
<dubbo:provider/> com.alibaba.dubbo.config.ProviderConfig 提供方配置 ProtocolConfigServiceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:service/> com.alibaba.dubbo.config.ServiceConfig 服务配置 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心。对应注解:@Service
<dubbo:consumer/> com.alibaba.dubbo.config.ConsumerConfig 消费方配置 ReferenceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:reference/> com.alibaba.dubbo.config.ReferenceConfig 引用配置 用于创建一个远程服务代理,一个引用可以指向多个注册中心。对应注解:@Reference
<dubbo:method/> com.alibaba.dubbo.config.MethodConfig 方法配置 用于 ServiceConfigReferenceConfig 指定方法级的配置信息
<dubbo:argument/> com.alibaba.dubbo.config.ArgumentConfig 参数配置 用于指定方法参数配置
<dubbo:module/> com.alibaba.dubbo.config.ModuleConfig 模块配置 用于配置当前模块信息,可选

下面是一些 dubbo 配置的总结:

协议

服务提供者

服务消费者

配置覆盖关系

配置覆盖关系:

  • 方法级优先,接口级次之,全局配置再次之。
  • 如果级别一样,则消费方优先,提供方次之。

规则二是指,所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方。其 URL 格式如下:protocol://username:password@host:port/path?key=value&key=value

dubbo 配置覆盖关系

参考:XML 配置

属性配置关系

dubbo 属性覆盖关系

参考:属性配置

注解配置实践

如果想用现代的 Java Config 替代传统的 XML 配置方式,配置如下:

声明组件

  • 服务提供方使用 @Service 注解暴露服务

    1
    2
    3
    4
    5
    6
    import com.alibaba.dubbo.config.annotation.Service;

    @Service(timeout = 5000)
    public class AnnotateServiceImpl implements AnnotateService {
    // ...
    }
  • 服务消费方使用 @Reference 注解引用服务

    1
    2
    3
    4
    5
    6
    7
    8
    import com.alibaba.dubbo.config.annotation.Reference;

    public class AnnotationConsumeService {
    @Reference
    public AnnotateService annotateService;

    // ...
    }

开启组件扫描

  • 2.5.7 (Nov, 2017) 以上版本,使用 @DubboComponentScan 指定 dubbo 组件扫描路径
  • 老版本或 Dubbox,使用:<dubbo:annotation package="com.alibaba.dubbo.test.service" />

Java Config 配置

  • 使用 @Configuration 注解开启 Java Config 并使用 @Bean 进行公共模块的 bean 配置,参考:API配置

自动化配置

  • 最后开启 @EnableDubboConfig

参考

在 Dubbo 中使用注解

https://www.oschina.net/news/92687/dubbo-spring-boot-starter-1-0-0

https://mvnrepository.com/artifact/com.alibaba.boot/dubbo-spring-boot-starter

https://mvnrepository.com/artifact/com.alibaba/dubbo

https://github.com/apache/incubator-dubbo

本文总结的一些学习笔记,用于建立安全观。

信任域与信任边界

  • 首先,安全问题的本质,是信任。一旦我们作为决策依据的条件被打破、被绕过,那么就会导致安全假设的前提条件不再可靠,变成一个伪命题。因此,把握住信任条件的度,使其恰到好处,正是设计安全方案的难点所在,也是安全这门学问的艺术魅力所在。
  • 通过一个安全检查(过滤、净化)的过程,可以梳理未知的人或物,使其变得可信任。被划分出来的具有不同信任级别的区域,我们称为信任域,划分两个不同信任域之间的边界,我们称为信任边界
  • 因为信任关系被破坏,从而产生了安全问题。我们可以通过信任域的划分、信任边界的确定,来发现问题是在何处产生的。
  • 数据从高等级的信任域流向低等级的信任域,是不需要经过安全检查的;数据从低等级的信任域流向高等级的信任域,则需要经过信任边界的安全检查。

安全基本三要素(CIA)

  • 机密性(Confidentiality):要求保护数据内容不能泄露,常见手段是加密。
  • 完整性(Integrity):要求保护数据内容是完整、没有被篡改的。常见手段是数字签名。
  • 可用性(Availability):要求保护资源是“随需而得”。如拒绝服务攻击 (简称DoS,Denial of Service) 破坏的是安全的可用性。
  • 真实性(Authenticity):通信双方的身份确认,确保数据来源于合法的用户。
  • 不可抵赖性(Non-repudiation),防抵赖。常见手段是数字签名。

认证(Authentication):我是谁?——身份

授权(Authorization):我能做什么?——权利

凭证(Credentials):依据是什么?——依据(凭证实现认证和授权的一种媒介,标记访问者的身份或权利)

3A 黄金法则

针对各个安全环节,可以使用 3A 黄金法则:

事前防御——认证(Authentication)

事中防御——授权(Authorization)

事后防御——审计(Audit)

认证、授权技术 HTTP 请求头
基本认证 Authorization: Basic <Base64("username:password")>
摘要认证 Authorization: Digest <MD5(username, password, nonce, ...)>
JWT Authorization: Bearer <JWT Token>
OAuth Authorization: Bearer <Access Token>

认证(Authentication)

认证其实包括两个部分:身份识别和认证。

  • 身份识别其实就是在问 “你是谁?”,你会回答 “你是你”。
  • 身份认证则会问 “你是你吗?”,那你要证明 “你是你” 这个回答是合法的。

身份识别和认证通常是同时出现的一个过程。身份识别强调的是主体如何声明自己的身份,而身份认证强调的是,主体如何证明自己所声明的身份是合法的。

比如说:

  • 当你在使用用户名和密码登录的过程中,用户名起到身份识别的作用,而密码起到身份认证的作用;
  • 当你用指纹、人脸或者门卡等进行登入的过程中,这些过程同时包含了身份识别和认证。

认证形式可以大致分为三种。按照认证强度由弱到强排序,分别是:

  • 你知道什么(密码、密保问题等);
  • 你拥有什么(门禁卡、手机验证码、安全令牌、U 盾等);
  • 你是什么(生物特征,如指纹、人脸、虹膜等)。

authentication

参考:认证技术总结

授权(Authorization)

在确认完 “你是你” 之后,下一个需要明确的问题就是 “你能做什么”。

除了对 “你能做什么” 进行限制,授权机制还会对 “你能做多少” 进行限制。比如:

  • 手机流量授权了你能够使用多少的移动网络数据。
  • 我们申请签证的过程,其实就是一次申请授权的过程。

参考:OAuth 2

审计(Audit)

当你在授权之下完成操作后,安全需要检查一下 “你做了什么”,这个检查的过程就是审计。

当发现你做了某些异常操作时,安全还会提供你做了这些操作的 “证据”,让你无法抵赖,这个过程就是问责。

安全评估的四阶段

  1. 资产等级划分:对资产进行等级划分,就是对数据做等级划分。当完成划分后,对要保护的目标数据已经有了一个大概的了解,接下来就是要划分信任域和信任边界了。

  2. 威胁分析:威胁(Threat)是指可能造成危害的来源。威胁分析即把所有的威胁都找出来。可以采用头脑风暴法。或采用 STRIDE 等模型。

STRIDE

  1. 风险分析:风险(Risk)是指可能会出现的损失。风险公式:Risk = Probability * Damage Potential,即影响风险高低的因素,除了造成损失的大小外,还需要考虑到发生的可能性。可以采用 DREAD 等模型。

DREAD

  1. 确认解决方案

安全方案设计的四原则

  • 默认安全性原则(Secure by Default),最基本也是最重要的原则。即:
    • 黑、白名单。随着防火墙、ACL 技术的兴起,使得直接暴露在互联网上的系统得到了保护。比如一个网站的数据库,在没有保护的情况下,数据库服务端口是允许任何人随意连接的;在有了防火墙的保护后,通过ACL可以控制只允许信任来源的访问。这些措施在很大程度上保证了系统软件处于信任边界之内,从而杜绝了大部分的攻击来源。因此如果更多地使用白名单(如防火墙、ACL),系统就会变得更安全。
    • 最小权限原则。安全设计的基本原则之一。最小权限原则要求系统只授予主体必要的权限,而不要过度授权,这样能有效地减少系统、网络、应用、数据库出错的机会。
  • 纵深防御原则 (Defense in Depth),其包含两层含义:
    • 首先,在各个不同层面、不同方面实施安全方案,避免出现疏漏,不同安全方案之间需要相互配合,构成一个整体;
    • 其次,在正确的地方做正确的事情,即:在解决根本问题的地方实施针对性的安全方案。
  • 数据与代码分离原则
    • 适用于各种由于“注入”而引发的安全问题,如 XSS、SQL 注入、CRLF 注入、X-Path 注入。
  • 不可预测性原则(Unpredictable)
    • 能有效地对抗基于篡改、伪造(如 CSRF)的攻击,其实现往往需用到加密算法、随机数算法、哈希算法等。

总结这几条原则:

  • Secure By Default:是时刻要牢记的原则;
  • 纵深防御:是要更全面、更正确地看待问题;
  • 数据与代码分离:是从漏洞成因上看问题;
  • 不可预测性:则是从克服攻击方法的角度看问题。

参考

互联网安全的核心问题,是数据安全的问题。

《白帽子讲 Web 安全》

https://time.geekbang.org/column/intro/262

同源策略

要了解什么是同源策略,先来看下如果没有同源策略,我们将遇到什么安全问题:

假设用户正在访问银行网站并且没有退出登录。然后,用户打开新 Tab 转到另一个恶意网站,其中有一些恶意 JavaScript 代码在后台运行并请求来自银行网站的数据。由于用户仍然在银行网站登录,恶意代码可以模拟用户在银行网站上做任何事情。这是因为浏览器可以向同域的银行网站发送和接收会话cookie。

同源策略(Same Origin Policy)是一种约定,是浏览器最核心也最基本的安全功能。如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,可以说 Web 是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。

同源策略限制了来自不同源站的“document”或脚本,对当前“document”读取或设置某些属性。受同源策略限制的元素:

  • Cookie
  • DOM
  • XMLHttpRequest
  • 第三方插件
    • Adobe Flash
    • Java Applet
    • Microsoft Silverlight
    • Google Gears
    • ……

一个域名的组成:

http://

Chrome 增强的网站隔离功能 [桌面版 / Android]

因为同源政策(Same Origin Policy)的存在,A 网站一般无法访问 B 网站存储在设备中的网站数据。不过因为安全漏洞的存在,部分恶意网站偶尔也可以绕过同源政策、获取这些文件并对其他网站进行攻击。

所以除了第一时间对各种浏览器安全漏洞进行修补,Chrome 也在早些时候引入了网站隔离功能:通过在独立进程中加载网站页面,确保网站与网站之间的数据安全性。在 Chrome 92 稳定版的桌面端,这个功能从浏览器标签页延伸到了浏览器扩展,不同扩展插件也将在独立进程中进行加载,同时几乎不会影响大部分扩展的现有功能。

出于性能考虑,网站隔离功能在 Android 版 Chrome 中一直都没有像桌面端那样全盘开启,仅在一些需要用户手动输入登录信息的网站中启用,并且仅支持拥有 2GB 及以上运行内存的设备。

本次 Chrome 92 则通过对 OAuth 2.0 协议和 Cross-Origin-Opener-Policy (COOP) 策略的额外支持扩展了网站隔离功能在移动端上的可用性和兼容性,换句话说,移动版 Chrome 现在能够识别并保护更多类型的网站了(比如采用第三方登录的那种)。

不过增强网站隔离功能在 Android 端的硬件限制依然为 2GB RAM,Google 同时也表示,如果你的设备可用内存足够,也可以通过 chrome://flags#enable-site-per-process 这一功能标签来手动开启全盘网站隔离。

2021-07-27 Updated

跨域资源共享

由于 XMLHttpRequest 受同源策略的约束,不能跨域访问资源,因此 W3C 委员会制定了 XMLHttpRequest 跨域访问标准 Cross-Origin Resource Sharing,通过目标域返回的 HTTP 头(Access-Control-Allow-Origin)来授权是否允许跨域访问。由于 HTTP 头对于 JavaScript 来说,一般是无法控制的,所以认为这个方案可以安全施行。

参考

https://en.wikipedia.org/wiki/Same-origin_policy

https://en.wikipedia.org/wiki/Cross-origin_resource_sharing

http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

http://blog.csdn.net/wang379275614/article/details/53333054

开会是一门学问,也是管理的必经之路。这里总结一点开会的小心得,目的是提升工作效率。

如何开好会?