MySQL 批量插入、批量更新

MySQL 批量插入/更新含有主键/唯一索引或者的解决方案是使用 on duplicate key update

解决方案

insert into database.table (id, key, field)
    values (v1, v2, v3), (v1, v2, v3), (v1, v2, v3)
    on duplicate key update
    id=values(id),key=values(key),field=values(field);

该方案适用范围广,
既可以批量插入,
也可以批量更新,
同时能保证避免插入重复数据,
还能避免影响自增 id,
又不需要创建临时表权限,
更不会修改已存在记录的其他字段。


实际应用中 update 后跟随的字段可以进行调整,保留只需要更新的字段


如果表中有自增 id,这个方案还有一点需要注意,insert into … values 跟随的记录数 (numberA)即为此次自增数 (numberA),即使存在重复唯一索引导致实际插入记录数(numberB)少于跟随的记录数(numberA)。


其他插入方案

1. insert ignore into

insert ignore 遇到重复数据会忽略,可以有效保证不插入重复数据,但也会忽略其他错误。
批量插入之后再进行单条更新。

# 批量插入,需要保证 NOT NULL 字段有值
insert into database.table (id, key, field)
    values (v1, v2, v3), (v1, v2, v3), (v1, v2, v3);

# 单条更新
update database.table set field = v3 where key = v1;
update database.table set field = v3 where key = v1;
update database.table set field = v3 where key = v1;

2. replace into

如果表中有主键或唯一索引,replace into 会删除冲突行(已存在记录),再新插入新行。因此导致 replace into 可能会导致主键自增 1。
如果表中没有主键和唯一索引,replace into 没有意义。

如果表中没有自增主键,使用 replace into 是一个不错的选择。

使用 replace 需要注意,如果 更新的字段 不全,会被设置为缺省值,也就是会清除其他字段。

replace into database.table (id, key, field)
    values (v1, v2, v3), (v1, v2, v3), (v1, v2, v3);

最后,推荐使用 insert into … on duplicate key update

on duplicate key update 是 mysql 特有语法。

MySQL root 密码丢失重置

前几周百度云同步时把重要文件误删,包括 mysql root 密码。

MySQL root 密码丢失,需要以类似 mysqld_safe –skip-grant-tables 方式运行,才能让 mysql -uroot 顺利执行,登录到 mysql 命令行。

如果 mysql 在运行,先将 mysql 运行方式记录下来

> ps aux | grep mysql
// 假设当前 mysql 运行命令如下,实际参数可能更复杂
/mysql/bin/mysqld --pid-file=/mysql/mysql.pid --socket=/tmp/mysql.sock

1. 关闭 mysql 进程

不要使用 kill 命令直接结束,而是通过 stop 参数结束 mysql 进程。

// 假设 mysql 启动命令是
// /etc/init.d/mysql start
// 则应该通过如下方式结束 mysql 进程
> /etc/init.d/mysql stop

2. 以 –skip-grant-tables 方式启动 mysql

在原有 mysql 运行命令基础上加上 –skip-grant-tables &,& 表示放入当前会话 background。

/mysql/bin/mysqld --pid-file=/mysql/mysql.pid --socket=/tmp/mysql.sock --skip-grant-tables &

3. 登录 mysql 修改 root 密码

> mysql -uroot
> update mysql.user set password=PASSWORD('MysqlRoot') where user='root';

4. 参考第 1 步,结束 mysql 进程

5. 执行原有 mysql 启动命令

// 假设 mysql 启动命令如下
> /etc/init.d/mysql start

使用 root 新密码登录 mysql

> mysql -uroot -p
Enter password: 

密码重置完成。

重置过程有两点需要注意:

  1. 不推荐使用 kill 结束 mysql 进程;
  2. 以 –skip-grant-tables 方式运行 mysql 需要保留原有参数

Linux 常见解压与压缩

Linux 压缩与解压命令

格式 解压 压缩
 
tar
(仅打包不压缩)
tar -xvf [原文件名].tar tar -cvf [目标文件名].tar [原文件名/目录名]
 
tar.bz2 bunzip2 [原文件名].tar.bz2 bzip2 [原文件名].tar
tar.bz2
(通过 tar 调用)
tar -jxvf [原文件名].tar.bz2 tar -jcvf [目标文件名].tar.bz2 [原文件名/目录名]
 
tar.gz gunzip [原文件名].tar.gz gzip [原文件名].tar
tar.gz
(通过 tar 调用)
tar -zxvf [原文件名].tar.gz tar -zcvf [目标文件名].tar.gz [原文件名/目录名]
 
tar.xz unxz [原文件名].tar.xz xz [原文件名].tar
tar.xz
(通过 tar 调用)
tar -Jxvf [原文件名].tar.xz tar -Jcvf [目标文件名].tar.xz [原文件名/目录名]
 
tar.Z
(已过时)
uncompress [原文件名].tar.Z compress [原文件名].tar
tar.Z
(通过 tar 调用)
tar -Zxvf [原文件名].tar.Z tar -Zcvf [目标文件名].tar.Z [原文件名/目录名]
 
7z 7z x [原文件名].7z 7z a [目标文件名].7z [原文件名/目录名]
 
jar jar -xvf [原文件名].jar jar -cvf [目标文件名].jar [原文件名/目录名]
 
zip unzip [原文件名].zip zip -r [目标文件名].zip [原文件名/目录名]

JavaScript 浮点数计算问题

JavaScript 浮点数神坑当属 0.1 + 0.2 == 0.3false

> 0.1 + 0.2 == 0.3
false
> 0.1 + 0.2
0.30000000000000004
>

还有一些比较隐蔽的问题,比如 Math.roundNumber.prototype.toFixed 也都不是能完全正常工作的。

> Math.round(1.105 * 100)
111
> Math.round(1.015 * 100)
101
> Math.round(1.025 * 100)
102
> 0.25.toFixed(1)
'0.3'
> 0.35.toFixed(1)
'0.3'

Math.round、toFixed 计算出错主要因为浮点数不能精确表示。

在这里 0.35 和 1.015 的值都不准确,一个办法是转换成整数计算再除以对应的十百千;另一个办法是采用现有的 lib,比如 accounting

> 1.015 * 100
101.49999999999999

代码运行环境

D:\node -v
v8.1.0

安装 Nginx、Lua,使用 Lua 扩展 Nginx

使用 lua-nginx-module 可以让 Nginx 通过一些指令如 content_by_lua、content_by_lua_file 运行 Lua 脚本,非常方便。

先下载所需文件

1. 安装 luajit,官网 http://luajit.org/install.html

解压之后直接 make

make PREFIX=/usr/local/luajit

2. 下载 ngx_devel_kit(github)、nginx_lua_module(github

3. 进入 nginx 源码目录准备 configure

如果已经安装 nginx,也可以再次 configure,但一定要涵盖上次使用的参数。

4. 导出 LUAJIT_LIB、LUAJIT_INC 环境变量

export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.0

LUAJIT_LIB 对应 libluajit-5.1.so 文件所在目录
LUAJIT_INC 对应 lua.h 文件所在目录

目录可能不一样,但参考 LUAJIT_LIB、LUAJIT_INC 含义。

5. 在 nginx-1.10.2 目录执行 ./configure

./configure \
--prefix=/usr/local/nginx \
--with-http_flv_module \
--with-http_gzip_static_module \
--with-http_realip_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_v2_module \
--with-ipv6 \
--with-ld-opt="-ljemalloc,-Wl,-rpath,/usr/local/luajit/lib" \
--with-openssl=/opt/src/openssl-1.0.2l \
--with-pcre=/opt/src/pcre-8.40 \
--with-pcre-jit \
--add-module=/opt/src/ngx_devel_kit-0.3.0 \
--add-module=/opt/src/lua-nginx-module-0.10.8

这里用到了 ljemalloc、openssl、pcre 可以预先安装

安装 jemalloc,进入 jemalloc-4.2.1

./configure
make
make install

安装 openssl pcre,或者进入源码目录安装

yum install openssl openssl-devel
yum install pcre pcre-devel

6. 测试执行 lua

在 Nginx 某个 server 下增加

location /hello { 
    content_by_lua 'ngx.say("Hello, Lua")'; 
}

尝试访问

[root@CentOS-58 06]# curl lua.zhengxianjun.com/hello
Hello, Lua!

安装成功并且正常运行。

如果对 nginx 安装目录进行版本管理,会发现 make install 完成之后只有 nginx/sbin/nginx 文件有改动。

安装完成之后切记要执行 sbin/ngxin -t 检查语法是否正确。
一定要安装正确才能执行 sbin/ngxin -s reload 。

因为对于已经安装的 nginx,如果 ./configure 没有覆盖上一次的参数,会导致某些功能失效。
第一次 configure 时没有添加 –with-http_ssl_module 导致 nginx 提示 unknown directive “ssl”。

babel-loader 生成多处 /******/ 前缀

使用 babel-loader 处理 .js 文件之后会在公共文件里多处行首添加 /******/,不明白为什么要这样做。

难道仅仅是为了区别框架代码和用户代码?

/******/ (function(modules) { // webpackBootstrap
/******/    // install a JSONP callback for chunk loading
/******/    var parentJsonpFunction = window["webpackJsonp"];
/******/    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
/******/        // add "moreModules" to the modules object,
/******/        // then flag all "chunkIds" as loaded and fire callback
/******/        var moduleId, chunkId, i = 0, callbacks = [];
/******/        for(;i < chunkIds.length; i++) {
/******/            chunkId = chunkIds[i];
/******/            if(installedChunks[chunkId])
/******/                callbacks.push.apply(callbacks, installedChunks[chunkId]);
/******/            installedChunks[chunkId] = 0;
/******/        }
/******/        for(moduleId in moreModules) {
/******/            modules[moduleId] = moreModules[moduleId];
/******/        }
/******/        if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
/******/        while(callbacks.length)
/******/            callbacks.shift().call(null, __webpack_require__);
/******/        if(moreModules[0]) {
/******/            installedModules[0] = 0;
/******/            return __webpack_require__(0);
/******/        }
/******/    };

目前应该没有参数可以控制移除,因为在文件 ./node_modules/webpack/lib/MainTemplate.js 中硬编码了这一段前缀

this.plugin("render", function(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) {
    var source = new ConcatSource();
    source.add("/******/ (function(modules) { // webpackBootstrap\n");
    source.add(new PrefixSource("/******/", bootstrapSource));
    source.add("/******/ })\n");
    source.add("/************************************************************************/\n");
    source.add("/******/ (");
    var modules = this.renderChunkModules(chunk, moduleTemplate, dependencyTemplates, "/******/ ");
    source.add(this.applyPluginsWaterfall("modules", modules, chunk, hash, moduleTemplate, dependencyTemplates));
    source.add(")");
    return source;
});

babel-loader 配置参考: API · Babel

PHP Bcrypt 更安全的密码加密机制

为了避免在服务器受到攻击,数据库被拖库时,用户的明文密码不被泄露,一般会对密码进行单向不可逆加密——哈希

常见的方式是:

哈希方式 加密密码
md5(‘123456’) e10adc3949ba59abbe56e057f20f883e
md5(‘123456’ . ($salt = ‘salt’)) 207acd61a3c1bd506d7e9a4535359f8a
sha1(‘123456’) 40位密文
hash(‘sha256’, ‘123456’) 64位密文
hash(‘sha512’, ‘123456’) 128位密文

密文越长,在相同机器上,进行撞库消耗的时间越长,相对越安全。

比较常见的哈希方式是 md5 + 盐,避免用户设置简单密码,被轻松破解。

password_hash

但是,现在要推荐的是 password_hash() 函数,可以轻松对密码实现加盐加密,而且几乎不能破解。

$password = '123456';

var_dump(password_hash($password, PASSWORD_DEFAULT));
var_dump(password_hash($password, PASSWORD_DEFAULT));

password_hash 生成的哈希长度是 PASSWORD_BCRYPT —— 60位,PASSWORD_DEFAULT —— 60位 ~ 255位。PASSWORD_DEFAULT 取值跟 php 版本有关系,会等于其他值,但不影响使用。

每一次 password_hash 运行结果都不一样,因此需要使用 password_verify 函数进行验证。

$password = '123456';

$hash = password_hash($password, PASSWORD_DEFAULT);
var_dump(password_verify($password, $hash));

password_hash 会把计算 hash 的所有参数都存储在 hash 结果中,可以使用 password_get_info 获取相关信息。

$password = '123456';
$hash = password_hash($password, PASSWORD_DEFAULT);
var_dump(password_get_info($hash));
输出
array(3) {
  ["algo"]=>
  int(1)
  ["algoName"]=>
  string(6) "bcrypt"
  ["options"]=>
  array(1) {
    ["cost"]=>
    int(10)
  }
}
注意不包含 salt

可以看出我当前版本的 PHP 使用 PASSWORD_DEFAULT 实际是使用 PASSWORD_BCRYPT。

password_hash($password, $algo, $options) 的第三个参数 $options 支持设置至少 22 位的 salt。但仍然强烈推荐使用 PHP 默认生成的 salt,不要主动设置 salt。

当要更新加密算法和加密选项时,可以通过 password_needs_rehash 判断是否需要重新加密,下面的代码是一段官方示例

$options = array('cost' => 11);
// Verify stored hash against plain-text password
if (password_verify($password, $hash))
{
    // Check if a newer hashing algorithm is available
    // or the cost has changed
    if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
    {
        // If so, create a new hash, and replace the old one
        $newHash = password_hash($password, PASSWORD_DEFAULT, $options);
    }
    // Log user in
}

password_needs_rehash 可以理解为比较 $algo + $option 和 password_get_info($hash) 返回值。

password_hash 运算慢

password_hash 是出了名的运行慢,也就意味着在相同时间内,密码重试次数少,泄露风险降低。

$password = '123456';
var_dump(microtime(true));
var_dump(password_hash($password, PASSWORD_DEFAULT));
var_dump(microtime(true));

echo "\n";

var_dump(microtime(true));
var_dump(md5($password));
for ($i = 0; $i < 999; $i++)
{
    md5($password);
}
var_dump(microtime(true));
输出
float(1495594920.7034)
string(60) "$2y$10$9ZLvgzqmiZPEkYiIUchT6eUJqebekOAjFQO8/jW/Q6DMrmWNn0PDm"
float(1495594920.7818)

float(1495594920.7818)
string(32) "e10adc3949ba59abbe56e057f20f883e"
float(1495594920.7823)

password_hash 运行一次耗时 784 毫秒, md5 运行 1000 次耗时 5 毫秒。这是一个非常粗略的比较,跟运行机器有关,但也可以看出 password_hash 运行确实非常慢。

Webpack 打包 css,z-index 被重新计算

使用 Webpack 打包 css 文件,发现打包后的 z-index 值跟源文件 z-index 不一致。

如下图,左侧是源文件,右侧是打包后的文件:

即使加上 !important,经过 OptimizeCssAssetsPlugin 调用 cssProcessor cssnano 处理之后也是 z-index: 2

因此,很可能是 cssnano 进行了重新计算(cssnano 称为 rebase),而且这种计算是不够准确的

因为打包后的文件有两处 z-index,这里是第二处,所以此处 z-index 是 2。

cssnano 将 z-index rebase 归类为 unsafe,而不是 bug,只有在单个网页的 css 全部写入一个 css 文件,并且不通过 JavaScript 进行改动时是 safe。

参考:http://cssnano.co/optimisations/zindex/

项目中提取了公共的 css,已经对 layout 设置了很小的 z-index,因此受到 cssnano z-index rebase 的影响。

cssnano 默认进行 z-index rebase。

unsafe (potential bug) 优化项默认不开启应该比较友好。

new OptimizeCssAssetsPlugin({
    cssProcessor: require('cssnano'),
    cssProcessorOptions: {
        discardComments: {removeAll: true},
        // 避免 cssnano 重新计算 z-index
        safe: true
    },
    canPrint: false
})

通过 Socket 获取网站 SSL 证书及公钥

通过 php curl 请求网页并不能获取到证书信息,此时需要使用 ssl socket 获取证书内容。

// 创建 stream context
$context = stream_context_create([
    'ssl' => [
        'capture_peer_cert' => true,
        'capture_peer_cert_chain' => true,
    ],
]);

$resource = stream_socket_client("ssl://$domain:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

$ssl = $cert['options']['ssl'];
$resource = $ssl['peer_certificate'];

// 网站证书中只有公钥,通过 openssl_pkey_get_details 导出公钥

$ret = [
    'crt' => '',
    'pub' => '',
];

$pkey = openssl_pkey_get_public($resource);
$ret['pub'] = openssl_pkey_get_details($pkey)['key'];

openssl_x509_export($resource, $pem);
$ret['crt'] = $pem;

foreach ($ssl['peer_certificate_chain'] as $resource)
{
    openssl_x509_export($resource, $pem);
    $ret['crt'] .= "\n" . $pem;
}

// 保存 $ret['crt'] 为 domain.crt
// 保存 $ret['pub'] 为 domain.pub

return $ret;

验证证书中的公钥A是否正确,通过私钥导出公钥B,比较两者发现一致。

$domain = 'blog.zhengxianjun.com';
$port = '443';
// ...
$pub_a = $ret['pub'];

$private_key_path = '/conf/ssl/blog.zhengxianjun.com.key';

// 证书没有设置密码,$passphrase 为空字符串
$pkey = openssl_pkey_get_private(file_get_content($private_key_path), $passphrase = '');
$pub_b = openssl_pkey_get_details($pkey)['key'];

// 两者一致
var_dump($pub_a === $pub_b);

函数 stream_socket_client 还有一个用途是当知道服务器 IP 时,能获取到服务器可能可以使用的域名。

$resource = stream_socket_client("ssl://$ip:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
$cert = stream_context_get_params($resource);

// 解析 X.509 格式证书
$info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);

// 获取证书中的可信域名列表
$domain = str_replace('DNS:', '', $info['extensions']['subjectAltName']);

以上可以看到获取网站证书并不能获得私钥。

在一些使用 CDN 的站点,如果使用了 HTTPS 同时又希望使用自有域名,是否需要将自己的私钥提供给 CDN 厂商呢?实际上证书路径与使用者名称(支持 https 的域名)并不需要一致。

也就是使用自有域名并进行 CDN 加速时不需要使用自有的 ssl 证书,只需将自己的 CDN 域名加到厂商证书的域名列表即可。

Closure-Compiler 指定 charset 减少输出文件大小

Google 的 Closure-Compiler  压缩 JavaScript 文件默认采用 UTF-8 作为输入编码,US_ASCII 作为输出编码。

–charset VAL : Input and output charset for all files
. By default, we accept UTF-8 as input
and output US_ASCII

因此,压缩汉字(或者日韩文字)之后,文件会变大。

执行命令 java -jar $dir/compiler.jar --js $cache_file --js_output_file $output_file

// 输入文件
var address = '上海';

// 输出文件
var address="\u4e0a\u6d77";

源文件较小时可以忽略这个问题,但源文件有大量汉字,可以指定 charset 避免输出文件过大。

执行命令 java -jar $dir/compiler.jar --js $cache_file --js_output_file $output_file --charset=UTF-8

// 输入文件
var address = '上海';

// 输出文件
var address="上海";

在开发中遇到一个 1.5MB 的地址文件,压缩之后变成 3MB,再经 gzip 压缩变成 780 KB 左右。而源文件经 gzip 之后在 590 KB。