TL;DR
PHP 中针对 Redis / MySQL 的长连接是生命周期级别的长连接,对于同一个进程的每一次请求都不会释放当前连接对象。而针对 TCP Socket 级别的连接是否已断开,则交给操作系统维持。
使用 PDO 对 MySQL 开启持久连接,要注意 PHP 执行的进程数量,不能超过 MySQL 设定的最大连接数。
上述结论的前提是使用 phpredis 扩展,PHP 版本为 5.4.41。
PHP 中针对 Redis / MySQL 的长连接是生命周期级别的长连接,对于同一个进程的每一次请求都不会释放当前连接对象。而针对 TCP Socket 级别的连接是否已断开,则交给操作系统维持。
使用 PDO 对 MySQL 开启持久连接,要注意 PHP 执行的进程数量,不能超过 MySQL 设定的最大连接数。
上述结论的前提是使用 phpredis 扩展,PHP 版本为 5.4.41。
MySQL InnoDB可以设定ANSI SQL-92中规定的四个事务隔离级别,事务并发度和事务隔离级别成反比,事务隔离级别越高,并发度越低。
是否有锁操作,取决于当前的读取是快照读,还是当前读,快照读读取的是可见的历史版本,无需上锁,简单的读操作是快照读(SELECT无附加语句),其他都是当前读,需要加锁。
对无索引的数据,InnoDB会锁定全表的记录,但是会在扫描过程中释放不符合筛选规则的记录的锁定。
对于有索引的数据,需要具体分析,在RR这一级别,InnoDB通过GAP锁机制避免了幻读问题。
中秋假期的前夕的9月12日,MySQL 8.0.0
放出了 Development Milestone Release。是开源数据库的一大新闻。
要知道MySQL的上一个版本号仅仅是 5.7
。在MySQL归属于Oracle公司之后,版本号的飞速提升也开始了,按此趋势,预计不日将会赶超Oracle自家商业数据库产品……
发布之际,简要的了解和比较一下 MySQL 5.5/5.6/5.7/8 之间的区别和特性。
如有错误,烦请指出。
在比较之前,首先提一下MySQL的开发周期.
MySQL一个大版本的开发,大致经历如下几个阶段:
也即是所谓的特性开发阶段。这个阶段是常规的功能点确定,代码开发,完成CR以及QA等常规的开发流程。
在一系列的测试以及bugfix之后,当QA “signs off” 即测试通过之后,会合并到TRUNK之中。
即特性测试阶段。
特性测试的阶段,从质量保证的角度出发,MySQL的测试工作将会有以下的关注点:
一个新特性达到要能被合并到TRUNK之中,需要满足如下条件:
作为应用的基础组件,性能是绝大多数开发人员都在关注的问题。在特性测试完成之后,将要进行的是性能测试。
性能测试主要关注两个指标:吞吐量
和响应时间
。
针对于吞吐量
的测试,会有如下的特点:
8
到1024
个连接sysbench
这类开源软件,进行简单OLTP(基本的事务处理)的测试,每个测试时间在5-10min范围内,随着数据集以及系统配置而定而对于响应时间
的测试,则会有如下的特点:
实验室发布是在用户对某一特性有着强烈的兴趣时,发布的快照版本,通常还没有合并到TRUNK之中。
实验室发布版本存在的特性,并不会保证在正式版等版本中存在。如果想要尝鲜,可以访问labs.mysql.com。
上述步骤结束之后,到了DMR阶段,也就是本次 8.0 版本所处的阶段。
DMR将会是一个重复迭代的阶段,下一个DMR版本将会包含上一个DMR版本的新特性或者功能修复。并且,DMR了的MySQL将会支持所有的平台,DMR状态下的MySQL的代码质量是达到了可供发布的水准的。
DMR看起来像是游戏的公测版本,每3-6个月会发布一个版本,每个DMR版本的MySQL会有一个特性需求收集截止时间点,新增的特性会历经开发、测试的历程,合并到TRUNK之中,变为下一个DMR。
总之,DMR的目的就是能够时常发布,让用户和客户能够反馈需求,体验新版本。
终于,在n个DMR之后,MySQL表现稳定,需求“无可”增加,客户满意,质量过关。
基于最后一个DMR版本,会发布GA版本,即我们可以“放心”使用在生产环境中的MySQL。
每个GA版本的发布周期间隔在18-24个月。
对于这些MySQL版本,如果想要了解之间的差异,一个可行的办法是阅读GA版本的Release Notes。
MySQL 的近几个版本发行时间间隔并不太大:
版本 | GA 版本发布时间 |
---|---|
5.5.8 | 2010-12-03 |
5.6.10 | 2013-02-05 |
5.7.9 | 2015-10-21 |
大约2年会有一个较大更新的版本会发出。
对于使用者而言,新特性应该是关注的第一焦点。下面会针对版本列出一些个人认为有特点的新特性。
InnoDB 因为支持事务、行级别锁而广为人知,并广泛应用。但是在之前的版本中,InnoDB并不是默认的存储引擎。在5.5中,InnoDB成为了默认的存储引擎。
半同步复制(Semisynchronous Replication)在MySQL 5.5中被支持(以插件形式实现)。
默认的MySQL通过异步模式进行复制,主库写入binlog之后,从库不一定能够被读取并处理,因为写入成功只是说明在主库上成功。主从不同步带来的问题相当之多,提升了开发难度。
而半同步复制则是主库需要有至少一个半同步从库,当一次写入操作进行之后,至少在主库和至少一个半同步从库上都完成了写入之后,用户才会收到已成功的信息。
半同步复制在这一程度上提高了数据的安全性。
MySQL 5.6 的主要变化在性能优化方面。有一些小的新特性也值得关注。
MySQL 5.5 中,如果设定多个Timestamp的属性为 ON UPDATE CURRENT_TIMESTAMP
时,这样的操作是不能完成的,这样的需求,通常要在业务代码中完成。
而到了 MySQL 5.6 中,这样的操作可以直接通过设定字段的属性即可完成。
全文索引 MyISAM 存储引擎之前相对于InnoDB的一个“优势”特性,在MySQL 5.6中不复存在。
针对字符串型的字段(CHAR
,VARCHAR
或者TEXT
),可以选择在创建表时增加这个类型的索引。也可以后续添加。
InnoDB的全文索引也使用的是倒排索引的设计,分词完成的词汇将会存储在独立的索引表之中。当包含全文索引的字段插入之后,会进行分词,同时先将分词结果进入内存缓存,之后再刷入索引表中,避免一次写入带来的大量附加的小规模的更改操作。
在MySQL 5.6中,会针对每一个数据库开启一个独立的复制线程,如果数据库压力平均的话,对于主从同步延迟会有一定的改善。但是如果数据操作都在一个数据库上,就不会有太多显著的效果了。
在MySQL 5.6前,如果从库宕机,重启之后需要进行同步,需要知道binlog文件名已经位置。
在MySQL 5.6中,加入了GTID(global transaction identifier)。GTID由source_id和transaction_id构成,source_id标识主库,transaction_id标识在数据库上进行的事务,格式即GTID = source_id:transaction_id
。
在加入GTID之后,重启从库之后,不需要重新进行位置的指向,只需要连接到主库即可,剩下的步骤将会是自动的。
InnoDB地位进一步增强,这一次系统表已然变成了基于InnoDB存储引擎的表。并且也不能禁用InnoDB存储引擎了。
在5.6中添加的多线程复制的增强版,针对每个数据库可以增加线程数进行同步,对5.7.9版本,在实际使用中,在机械盘的服务器上,原有业务高峰时主从同步延迟在10-30分钟左右,使用5.7.9之后基本实现了数据上的同步。
即将多个主库的数据归并到一个从库的实例上。
之前的MySQL,每个从库都只能拥有一个主库,如今MySQL提供了官方的解决方案,用于将多个主库的数据同步到一个从库之上。
多源复制有一个关键概念,即频道(channel)。频道指代一个主从库之间用于同步binlog的连接,通过新增的FOR CHANNEL
子句,指定一个非空的频道名称,按照先前版本的连接主库的方法,即可实现多源复制功能。
需要注意的是,当多个主库均写入同一张表时,是要自行处理主键冲突。
PostgreSQL 9.3开始,PostgreSQL中JSON成为了内置的数据类型。
作为被广泛使用的数据组织格式,之前版本的只能讲JSON格式数据按照字符串形式进行存储。
到了5.7之后,JSON支持也被加入。
JSON中的字符串在MySQL中会被转化成utf8mb4
的字符集,给携带诸如emoji字符的数据的存储带来了方便。
对于JSON数据的结构特性,MySQL中对JSON的查询需要借助path expression以及JSON_EXTRACT
方法进行查询。path expression的简要要点如下:
$
符号开头.
符号紧接着的是对象中的key[n]
中表示的是数组中的第n个元素,n>=0.[*]
表示一个key下的所有对象[*]
表示一个key下所有的数组exp_a**exp_b
则表示path中带有exp_a
与exp_b
的值更多操作参见手册。
在之前的版本中,innodb_buffer_pool_size
调整之后,需要重启数据库实例,这个对于线上业务几乎是不可接受的。硬件性能强悍的服务器,调整这一参数之后,MySQL的表现会有较为客观的提升。
到了MySQL 5.7,这一参数终于可以在线调整了。
在之前的版本中,初始化系统表一般都会使用mysql_install_db
脚本,到MySQL 5.7之后建议使用mysqld --initialize
完成实例初始化。
在通过mysqld --initialize
进行初始化时,需要加上--initial-insecure
才能实现空密码登录,否则会将初始化的默认密码写入到错误文件中。
初始化完成之后,还需要使用MySQL 5.7版本的客户端登录,并且修改默认密码。
作为版本号突飞猛进的一个版本,在MySQL 8.0中新增了如下的特性:
8.0中将会增强账号管理的功能,提供角色
这一概念,即能组合权限,批量授权给某一用户。
innodb_deadlock_detect
,可以在高并发系统中动态调整这一特性,提升性能->>
操作符,使用这一操作符等同于对JSON_EXTRACT
的结果进行JSON_UNQUOTE
操作,简化了SQL语句JSON_ARRAYAGG
与JSON_OBJECTAGG
。JSON_ARRAYAGG
将某列的值按照一个JSON数据返回,而JSON_OBJECTAGG
将列A作为键,列B作为值,返回一个JSON对象格式的数据后续将会继续更新本文。
开发过程中经常会使用MySQL的LOAD DATA功能,用于导入文件到MySQL的指定数据库表中。
若已经将文件切分为N个小文件再进行LOAD操作(例如使用Linux下的 split
工具),那么进度还是很容易把控的,可以通过直接查找当前正在进行导入的分片,进而判断当前的分片。
可是,如果某些情况下直接对一个大型的文件进行进行LOAD操作,整个过程并不能直观的获取当前的进度的,需要通过一些相对曲折的过程才能获取当前LOAD的进度。
Linux中的/proc
虚拟文件系统是一个非常有趣的部分,这一个目录并不是包含了一些常规意义上的文件,而是表征了进程的部分运行时信息。部分Linux工具更是可以直接用读取目录中的部分信息来替代[1]。
在/proc
下可以看到大量的名为数字的目录,这些数字正是进程的pid。而cd到其中任何一个目录下,可以看到类似的信息:
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 |
/proc/18458$ ll 总用量 0 dr-xr-xr-x 2 root root 0 3月 29 10:48 attr -rw-r--r-- 1 root root 0 4月 3 21:34 autogroup -r-------- 1 root root 0 4月 3 21:34 auxv -r--r--r-- 1 root root 0 4月 3 21:34 cgroup --w------- 1 root root 0 4月 3 21:34 clear_refs -r--r--r-- 1 root root 0 1月 27 14:44 cmdline -rw-r--r-- 1 root root 0 4月 3 21:34 comm -rw-r--r-- 1 root root 0 4月 3 21:34 coredump_filter -r--r--r-- 1 root root 0 4月 3 21:34 cpuset lrwxrwxrwx 1 root root 0 4月 3 21:34 cwd -> / -r-------- 1 root root 0 3月 18 10:31 environ lrwxrwxrwx 1 root root 0 2月 23 13:24 exe -> /usr/local/nginx/sbin/nginx dr-x------ 2 root root 0 3月 29 10:48 fd dr-x------ 2 root root 0 4月 3 21:34 fdinfo -r-------- 1 root root 0 4月 3 21:34 io -rw------- 1 root root 0 4月 3 21:34 limits -rw-r--r-- 1 root root 0 4月 3 21:34 loginuid -r--r--r-- 1 root root 0 4月 3 21:34 maps -rw------- 1 root root 0 4月 3 21:34 mem -r--r--r-- 1 root root 0 4月 3 21:34 mountinfo -r--r--r-- 1 root root 0 4月 3 21:34 mounts -r-------- 1 root root 0 4月 3 21:34 mountstats dr-xr-xr-x 5 root root 0 4月 3 21:34 net dr-x--x--x 2 root root 0 4月 3 21:34 ns -r--r--r-- 1 root root 0 4月 3 21:34 numa_maps -rw-r--r-- 1 root root 0 4月 3 21:34 oom_adj -r--r--r-- 1 root root 0 4月 3 21:34 oom_score -rw-r--r-- 1 root root 0 4月 3 21:34 oom_score_adj -r--r--r-- 1 root root 0 4月 3 21:34 pagemap -r--r--r-- 1 root root 0 4月 3 21:34 personality lrwxrwxrwx 1 root root 0 4月 3 21:34 root -> / -rw-r--r-- 1 root root 0 4月 3 21:34 sched -r--r--r-- 1 root root 0 4月 3 21:34 schedstat -r--r--r-- 1 root root 0 4月 3 21:34 sessionid -r--r--r-- 1 root root 0 4月 3 21:34 smaps -r--r--r-- 1 root root 0 4月 3 21:34 stack -r--r--r-- 1 root root 0 1月 27 14:44 stat -r--r--r-- 1 root root 0 1月 27 15:55 statm -r--r--r-- 1 root root 0 1月 27 14:44 status -r--r--r-- 1 root root 0 4月 3 21:34 syscall dr-xr-xr-x 3 root root 0 4月 3 21:34 task -r--r--r-- 1 root root 0 4月 3 21:34 wchan |
各个目录的说明可以参考此处。
这里我们关注的地方是如何通过这些丰富的信息获取导入数据库的进度。
考虑到这一导入操作,实际上是利用了MySQL进行读取文件的操作,那么,只需要知道MySQL当前读取的文件位置,就可以了解到当前的进度了。
/proc/[PID]/fdinfo/
这一目录正是解决这一问题的关键,这一目录包含了当前进程已打开的文件的信息,其中文件名正是文件描述符的名称,而相关信息则存储在这个只读文件之中。包含的信息形如:
1 2 3 |
/proc/18458/fd$ cat ../fdinfo/7 pos: 2293603 flags: 0102001 |
pos
即文件读取游标的偏移值,也就是我们关注的已读取到的位置。
flags
则是一个八进制数,表征当前文件的打开状态。
以上述打开的文件为例,这是一个Nginx打开的日志文件,通过lsof +fg -p [PID]
可以看到这一文件打开使用的flag:
1 2 3 |
COMMAND PID USER FD TYPE FILE-FLAG DEVICE SIZE/OFF NODE NAME ... nginx 18458 root 7w REG W,AP,LG 8,7 2293603 4194313 /home/www/logs/access_log |
可以看到使用了W、AP、LG三个flag,而W
对应的是O_WRONLY
,AP
对应的是O_APPEND
,LG
对应的的O_LARGEFILE
,这三个常量的值一般可以在/usr/include/bits/fcntl.h
中找到:
1 2 3 |
bits/fcntl.h:36:#define O_WRONLY 01 bits/fcntl.h:42:#define O_APPEND 02000 bits/fcntl.h:71:# define O_LARGEFILE 0100000 |
所以flags的值为何是0102001
也可以解释了。
根据上述分析,首先我们直接找到正在进行LOAD操作的MySQL进程的PID,获取之后查看当前打开的文件(假设文件名为foo)在进程中的fd:
1 |
ls -l /proc/[PID]/fd | grep foo | grep -v grep | awk '{print $9}' |
获取fd之后,直接读取对应的fdinfo:
1 |
cat /proc/[PID]/fdinfo/[fd] |
根据pos
可以知道当前已读取了的文件位置,进而获知LOAD进度。
以上。
之前有看到过对于emoji表情这类字符,想要存入MySQL之中,需要建立或者修改表的字符集为utf8mb4
,而且对MySQL版本还有一定的要求(>=5.5.3
),否则MySQL会提示类似:
1 |
ERROR 1366: Incorrect string value: '\xF0\x9F\x8D\xBA' for column |
的错误。
然而在PHP中其实有一种情况是可以“存储”emoji表情字符的,那就是将包含emoji的字符串通过PHP的json_encode
方法处理后再进行存储操作。
根据维基百科上对于字符编码
的定义,我们可以将字符集
和字符编码
看做同义词,后面二者将会不加区分的使用。
emoji不能直接存入UTF-8字符集的表中的原因,就得从emoji是什么说起。
emoji
简而言之就是若干组Unicode
字符,随着iPhone、微博、微信等硬件、软件的支持、普及,emoji也算是越来越常见。
这类字符因为其码位值的原因(U+1F300..1F545
,U+1F600..1F641
,U+1F300-1F5FF
,U+1F600-1F64F
,U+1F300-1F5FF
,U+1F600-1F64F
),是无法通过3字节长度的UTF-8编码表示的。
以啤酒(BEER MUG
)符号举例,这个符号的Unicode的码位值为U+1F37A
,那么转换成UTF-8编码则是:
0xF0
0x9F
0x8D
0xBA
明显可以看出需要通过4个字符进行表示,而在MySQL中,一般DBA给定的默认字符集都是UTF-8,而MySQL的文档中写道:
The utf8 character set is the same in MySQL 5.6 as before 5.6 and has exactly the same characteristics:
No support for supplementary characters (BMP characters only).
A maximum of three bytes per multibyte character.
也就是说直到5.6及以前的MySQL的UTF-8字符集最大只支持3个字符,那么emoji字符无法直接存入也是可以理解的了。
从上一主题可以看到,不能直接存储emoji字符的话,那么是不是可以通过其他间接手段对emoji字符进行存储呢?
答案是肯定的,如果能够对emoji字符转化成为其编码的字符串之后进行存储,在展示时从字符串转化为原始字符即可,或者是将其转化成为一些特别的字符串进行处理,总而言之就是
1 |
原始字符->字符串->MySQL->字符串->原始字符 |
的过程。
在实际项目中,JSON是常见一种数据格式,很多结构化的数据可以通过JSON进行存储,而PHP的JSON处理极其简便,配合array简直不能更轻松,很多情况下,数组数据可以通过json_encode之后当做字符串存入MySQL中。
然而这与emoji有什么关系呢?
答案是json_encode会对作为数组中值的emoji字符进行转化,之后转化结果字符串可以方便的存入数据库中。
翻阅在使用的PHP 5.4.24
的源码,阅读json扩展的源码ext\json\json.c
,可以看到php_json_encode
函数中的位于文件的629
~631
行:
1 2 3 |
case IS_STRING: json_escape_string(buf, Z_STRVAL_P(val), Z_STRLEN_P(val), options TSRMLS_CC); break; |
跟进到json_escape_string
函数,可以看到文件的422
行:
1 |
ulen = json_utf8_to_utf16(utf16, s, len); |
即会将UTF-8编码的字符转化为UTF-16的编码。
之后423
到538
行将转化结果添加到返回的字符串buf
中(详情参见PHP源代码文件)。
实际操作一下:
这样就解释了为什么经过json_encode处理之后的字符可以存入MySQL中,当然,这样处理之后的成本之一就是存储的字节数的增加了。