- Instruction
- Requirement
- 建立測試表
- 在linux中編譯ycdb
- Start ycdatabase
- 初始化ycdb連線
- 原生SQL執行
- 錯誤處理
- Where 語句
- Select 語句
- Insert 語句
- Replace 語句
- Update 語句
- Delete 語句
- 完整例句
- 資料庫事務
- 資料快取
- MySQL資料庫連線池
- Redis 連線池方案
MySQL資料庫連線池
php資料庫連線池的缺陷
短連線效能普遍上不去,CPU 大量資源被系統消耗,網路一旦抖動,會有大量 TIME_WAIT 產生,不得不定期重啟服務或定期重啟機器,伺服器工作不穩定,QPS 忽高忽低,穩定高效的連線池可以有效的解決上述問題,它是高併發的基礎。
ycdb通過一種特殊的方式來建立一個穩定的與MySQL之間的連線池,效能至少提升30%,按照 PHP 的執行機制,長連線在建立之後只能寄居在工作程式之上,也就是說有多少個工作程式,就有多少個長連線,打個比方,我們有 10 臺 PHP 伺服器,每臺啟動 1000 個 PHP-FPM 工作程式,它們連線同一個 MySQL 例項,那麼此 MySQL 例項上最多將存在 10000 個長連線,數量完全失控了!而且PHP的連線池心跳機制不完善。
解決方案
原始碼 github 地址: https://github.com/caohao-php/ycdatabase
我們不妨繞著走。讓我們把目光聚焦到 Nginx 的身上,其 stream 模組實現了 TCP/UDP 服務的負載均衡,同時藉助 stream-lua 模組,我們就可以實現可程式設計的 stream 服務,也就是用 Nginx 實現自定義的 TCP/UDP 服務!當然你可以自己從頭寫 TCP/UDP 服務,不過站在 Nginx 肩膀上無疑是更省時省力的選擇。我們可以選擇 OpenResty 庫來完成MySQL的連線池功能,OpenResty是一個非常強大,而且功能完善的Nginx Lua框架,他封裝了Socket、MySQL, Redis, Memcache 等操作,可是 Nginx 和 PHP 連線池有什麼關係?且聽我慢慢道來:通常大部分 PHP 是搭配 Nginx 來使用的,而且 PHP 和 Nginx 多半是在同一臺伺服器上。有了這個客觀條件,我們就可以利用 Nginx 來實現一個連線池,在 Nginx 上完成連線 MySQL 等服務的工作,然後 PHP 通過本地的 Unix Domain Socket 來連線 Nginx,如此一來既規避了短連結的種種弊端,也享受到了連線池帶來的種種好處。
OpenResty 安裝OpenResty 文件: https://moonbingbing.gitbooks.io/openresty-best-practices/content/openresty/install_on_centos.html
CentOS 6.8 下的安裝:
安裝必要的庫
$ yum install readline-devel pcre-devel openssl-devel perl
安裝 OpenResty
$ cd ~/ycdatabase/openresty
$ tar -xzvf openresty-1.13.6.1.tar.gz
$ cd openresty-1.13.6.1
$ ./configure --prefix=/usr/local/openresty.1.13 --with-luajit --without-http_redis2_module --with-http_iconv_module
$ gmake
$ gmake install
開啟資料庫連線池
$ cp -rf ~/ycdatabase/openresty/openresty-pool ~/
$ /usr/local/openresty.1.13/nginx/sbin/nginx -p ~/openresty-pool
MySQL資料庫連線池配置
~/openresty-pool/conf/nginx.conf ,
如果你有多個 MySQL, 你可以另起一個 server , 並在listen unix 之後新增一個新的unix domain socket監聽。
worker_processes 1; #nginx worker 數量
error_log logs/error.log; #指定錯誤日誌檔案路徑
events {
worker_connections 1024;
}
stream {
lua_code_cache on;
lua_check_client_abort on;
server {
listen unix:/tmp/mysql_pool.sock;
content_by_lua_block {
local mysql_pool = require "mysql_pool"
local config = {host = "127.0.0.1",
user = "root",
password = "test",
database = "collect",
timeout = 2000,
max_idle_timeout = 10000,
pool_size = 200}
pool = mysql_pool:new(config)
pool:run()
}
}
}
PHP程式碼
- 除了option 配置為 array("unix_socket" => "/tmp/mysql_pool.sock") 之外,php的mysql連線池使用方法和之前一模一樣,另外, unix_socket 方式的 MySQL 不支援事務。
$option = array("unix_socket" => "/tmp/mysql_pool.sock");
$ycdb = new ycdb($option);
$ret = $ycdb->select("user_info_test", "*", ["sexuality" => "male"]);
if($ret == -1) {
$code = $ycdb->errorCode();
$info = $ycdb->errorInfo();
echo "code:" . $code . "\n";
echo "info:" . $info[2] . "\n";
} else {
print_r($ret);
}
Lua資料庫連線池程式碼
~/openresty-pool/mysql_pool.lua
local mysql = require "resty.mysql"
local cjson = require "cjson"
local assert = assert
local setmetatable = setmetatable
local tonumber = tonumber
-- 解析請求
local function parse_request(sock)
--獲取 sql 語句
local sql_size, err = sock:receive()
if not sql_size then
if err == "timeout" then
sock:close()
end
return nil, err
end
local size = tonumber(sql_size)
if size <= 0 then
return nil, "SQL size is zero"
end
local sql_str, err = sock:receive(size)
if not sql_str then
if err == "timeout" then
sock:close()
end
return nil, err
end
--獲取 map
local map_size, err = sock:receive()
if not map_size then
if err == "timeout" then
sock:close()
end
return nil, err
end
size = tonumber(map_size);
if size <= 0 then
-- 沒有 map
return sql_str
end
local map_res, err = sock:receive(map_size)
if not map_res then
if err == "timeout" then
sock:close()
end
return nil, err
end
-- 解析 map ,建立 SQL 語句,防止SQL隱碼攻擊
local maps = cjson.decode(map_res)
for k, v in pairs(maps) do
if v == true then
v = 1
elseif v == false then
v = 0
end
sql_str = ngx.re.gsub(sql_str, k, ngx.quote_sql_str(v))
end
return sql_str
end
-- 返回請求
local function response_success(sock, result)
local ret = {
errno = 0,
data = result
}
local send_str = cjson.encode(ret)
local ret, err = sock:send(string.len(send_str) .. "\n" .. send_str)
if not ret then
ngx.log(ngx.ERR, "response success failed : [", err, "], send_str=[", send_str, "]")
return nil, err
end
end
-- 返回請求
local function response_error(sock, errno, errmsg, sqlstate)
local ret = {
errno = errno,
errorCode = sqlstate,
errorInfo = {sqlstate, errno, errmsg}
}
local send_str = cjson.encode(ret)
local ret, err = sock:send(string.len(send_str) .. "\n" .. send_str)
if not ret then
ngx.log(ngx.ERR, "response error failed : [", err, "], send_str=[", send_str, "]")
return nil, err
end
end
-- 關閉資料庫
local function close_db(db)
if not db then
return
end
db:close()
end
-- 異常退出
local function exit(err)
ngx.log(ngx.ERR, "ERROR EXIT: [", err, "]")
return ngx.exit(ngx.ERROR)
end
----------------------------------------
local _M = {}
_M._VERSION = "1.0"
function _M.new(self, config)
local t = {
_host = config.host,
_port = config.port or 3306,
_user = config.user,
_password = config.password,
_database = config.database,
_timeout = config.timeout or 2000, -- default 2 sec
_pool_size = config.pool_size or 100,
_max_idle_timeout = config.max_idle_timeout or 10000
}
return setmetatable(t, { __index = _M })
end
function _M.run(self)
local downstream_sock = assert(ngx.req.socket(true))
local query_sql, err = parse_request(downstream_sock)
if not query_sql then
return exit("parse_request failed : " .. err)
end
--資料庫連線
local db, err = mysql:new()
db:set_timeout(self._timeout)
local ok, err, errno, sqlstate = db:connect{
host = self._host,
port = self._port,
database = self._database,
user = self._user,
password = self._password,
max_packet_size = 1024 * 1024}
if not ok then
response_error(downstream_sock, -1, err, "E0001")
return exit("connect mysql error : " .. err)
end
local result, err, errno, sqlstate = db:query(query_sql)
-- 返回結果
if result then
response_success(downstream_sock, result)
else
ngx.log(ngx.ERR, "query failed: [", errno, "][", sqlstate, "][",err , "]")
response_error(downstream_sock, errno, err, sqlstate)
end
-- 設定 mysql 連線池
local ok, err = db:set_keepalive(self._max_idle_timeout, self._pool_size)
if not ok then
ngx.log(ngx.ERR, "set_keepalive failed: [", err, "]")
end
end
return _M
Redis連線池方案
同理,Redis也可以採用相同的方法解決連線池問題。
Redis連線池配置
~/openresty-pool/conf/nginx.conf ,
worker_processes 1; #nginx worker 數量
error_log logs/error.log; #指定錯誤日誌檔案路徑
events {
worker_connections 1024;
}
stream {
lua_code_cache on;
lua_check_client_abort on;
server {
listen unix:/tmp/redis_pool.sock;
content_by_lua_block {
local redis_pool = require "redis_pool"
pool = redis_pool:new({ip = "127.0.0.1", port = 6379, auth = "password"})
pool:run()
}
}
server {
listen unix:/tmp/mysql_pool.sock;
content_by_lua_block {
local mysql_pool = require "mysql_pool"
local config = {host = "127.0.0.1",
user = "root",
password = "test",
database = "collect",
timeout = 2000,
max_idle_timeout = 10000,
pool_size = 200}
pool = mysql_pool:new(config)
pool:run()
}
}
}
PHP程式碼
$redis = new Redis();
$redis->pconnect('/tmp/redis_pool.sock');
var_dump($redis->hSet("foo1", "vvvvv42", 2));
var_dump($redis->hSet("foo1", "vvvv", 33));
var_dump($redis->expire("foo1", 111));
var_dump($redis->hGetAll("foo1"));
Redis連線池Lua程式碼
~/openresty-pool/redis_pool.lua
local redis = require "resty.redis"
local assert = assert
local rawget = rawget
local setmetatable = setmetatable
local tonumber = tonumber
local byte = string.byte
local sub = string.sub
-- 解析請求
local function parse_request(sock)
local line, err = sock:receive()
if not line then
if err == "timeout" then
sock:close()
end
return nil, err
end
local result = line .. "\r\n"
local prefix = byte(line)
if prefix == 42 then -- char '*'
local num = tonumber(sub(line, 2))
if num <= 0 then
return result
end
for i = 1, num do
local res, err = parse_request(sock)
if res == nil then
return nil, err
end
result = result .. res
end
elseif prefix == 36 then -- char '$'
local size = tonumber(sub(line, 2))
if size <= 0 then
return result
end
local res, err = sock:receive(size)
if not res then
return nil, err
end
local crlf, err = sock:receive(2)
if not crlf then
return nil, err
end
result = result .. res .. crlf
end
return result
end
-- 異常退出
local function exit(err)
ngx.log(ngx.ERR, "Redis ERROR EXIT: [", err, "]")
return ngx.exit(ngx.ERROR)
end
----------------------------------------
local _M = {}
_M._VERSION = "1.0"
function _M.new(self, config)
local t = {
_ip = config.ip or "127.0.0.1",
_port = config.port or 6379,
_auth = config.auth,
_timeout = config.timeout or 1000, -- default 1 sec
_pool_size = config.pool_size or 100,
_max_idle_timeout = config.max_idle_timeout or 10000
}
return setmetatable(t, { __index = _M })
end
function _M.run(self)
local downstream_sock = assert(ngx.req.socket(true))
-- 解析客戶端請求
local res, err = parse_request(downstream_sock)
if not res then
return exit("parse_request failed : " .. err)
end
-- 建立 redis 連線
local red = redis:new()
red:set_timeout(self._timeout)
local ok, err = red:connect(self._ip, self._port)
if not ok then
return exit(err)
end
-- redis auth 授權
if self._auth then
local times = assert(red:get_reused_times())
if times == 0 then
local ok, err = red:auth(self._auth)
if not ok then
return exit("auth failed : " .. err)
end
end
end
-- 傳送請求到 redis
local upstream_sock = rawget(red, "_sock")
upstream_sock:send(res)
-- 接收 redis 應答,並解析
local res, err = parse_request(upstream_sock)
if not res then
return exit("receive from redis server error: " .. err)
end
-- 傳送應答給客戶端
downstream_sock:send(res)
-- 設定 redis 連線池
local ok, err = red:set_keepalive(self._max_idle_timeout, self._pool_size)
if not ok then
ngx.log(ngx.ERR, "redis set_keepalive failed: [", err, "]")
end
end
return _M