C語言PHP擴展高性能數據庫ORM框架ycdb(2) : 構建穩定的數據庫連接池

上一章:C語言PHP擴展高性能數據庫ORM框架ycdb(1) : SQL生成器

下一章:C語言PHP擴展高性能數據庫ORM框架ycdb(3): 穩定的Redis連接池

目錄:

  • Instruction
  • Requirement
  • 創建測試表
  • 在linux中編譯ycdb
  • Start ycdatabase
  • 初始化ycdb連接
  • 原生SQL執行
  • 錯誤處理
  • Where 語句
  • Select 語句
  • Insert 語句
  • Replace 語句
  • Update 語句
  • Delete 語句
  • 完整例句
  • 數據庫事務
  • 數據緩存
  • PHP數據庫連接池
  • 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 ~/
$ mkdir ~/openresty-pool/logs
$ /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

 

上一章:開源輕量級PHP數據庫ORM框架ycdb(2) : C語言擴展基礎函數

下一章:開源輕量級PHP數據庫ORM框架ycdb(3): 穩定的Redis連接池

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章