UDF是mysql的一個拓展接口析孽,UDF(Userdefined function)用戶自定義函數(shù)勿她。在什么地方使用這個功能呢术奖,試想有如下場景:
你的網(wǎng)站使用mysql作為最終數(shù)據(jù)落地的存儲引擎腰奋,而redis作為緩存以減小查詢請求穿透到mysql的數(shù)量,可以極大的降低數(shù)據(jù)庫性能瓶頸帶來的整個網(wǎng)站對外服務(wù)的卡頓阳谍、不可用等情況蛀柴。這種方式的架構(gòu),當(dāng)有查詢請求的時候矫夯,我們可以在業(yè)務(wù)邏輯層控制鸽疾,先從緩存中查詢,無命中的情況下训貌,再到數(shù)據(jù)庫中查詢制肮,同時緩存到redis中;當(dāng)有修改請求的時候递沪,我們可以先修改數(shù)據(jù)庫豺鼻,然后刪除或更新緩存。
以上方式是我們業(yè)務(wù)量不大款慨,開發(fā)簡單的儒飒,少橫向擴(kuò)展的情況下做的。當(dāng)開發(fā)復(fù)雜度隨著業(yè)務(wù)量并發(fā)增大檩奠,呈現(xiàn)橫向擴(kuò)展和垂直方向上螺旋迭代上升趨勢的時候约素,邏輯復(fù)雜度直線上升。還采用在業(yè)務(wù)邏輯層做緩存控制將變得很復(fù)雜笆凌,運(yùn)維上也容易出錯。
這個時候士葫,如果能將緩存邏輯和業(yè)務(wù)邏輯分離乞而,緩存層對業(yè)務(wù)邏輯提供服務(wù)透明,業(yè)務(wù)邏輯不用關(guān)心緩存邏輯慢显,緩存邏輯也不用隨業(yè)務(wù)變化而改動爪模,互相做自己的事情欠啤,這樣高內(nèi)聚低耦合可以極大的增加擴(kuò)展性和健壯性,也是我們做架構(gòu)應(yīng)該努力發(fā)展的方向屋灌。
那么具體來說洁段,我的這里要做的事情,其實就是把緩存更新的邏輯共郭,放到mysql中去做祠丝。寫一個trigger觸發(fā)器監(jiān)控insert/update/delete這些修改數(shù)據(jù)的操作,當(dāng)有修改操作的時候除嘹,調(diào)用對應(yīng)的自定義UDF函數(shù)來遠(yuǎn)程回寫redis緩存写半,而我們在業(yè)務(wù)邏輯層則只管更新數(shù)據(jù)就行了,緩存更新的操作都放給以上的緩存層邏輯來完成尉咕。
當(dāng)然叠蝇,以上操作也可以反方向來,先寫redis年缎,然后由redis同步到mysql去悔捶。兩種方式各有利弊,看你的具體場景如何選擇了单芜,這里不討論蜕该。我們此處的目的是使用mysql的udf函數(shù)更新數(shù)據(jù)到redis中。
開發(fā)環(huán)境
操作系統(tǒng):centos 6.4 server缓溅,內(nèi)核2.6.32-358.18.1.el6.x86_64
編譯器:gcc 4.4.7
數(shù)據(jù)庫:mysql server 5.1.73
mysql服務(wù)器之前已經(jīng)安裝了蛇损,現(xiàn)在我們要安裝mysql開發(fā)包
yum install mysql-devel -y
一、UDF函數(shù)入門
首先學(xué)習(xí)下UDF函數(shù)的使用方法坛怪。我們自定義一個函數(shù)文件淤齐,test_add.cpp如下
#include <mysql.h>
extern "C" long long testadd(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
int a = *((long long *)args->args[0]);
int b = *((long long *)args->args[1]);
return a + b;
}
extern "C" my_bool testadd_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
return 0;
}
這是c++代碼,編譯
[root@centos6 ~]# g++ -shared -fPIC -I /usr/include/mysql -o test_add.so test_add.cpp
拷貝到mysql插件目錄下袜匿,要以root身份
[root@centos6 ~]# cp test_add.so /usr/lib64/mysql/plugin/
如果你不知道插件的路徑更啄,執(zhí)行
mysql> show variables like '%plugin%';
+---------------+-------------------------+
| Variable_name | Value |
+---------------+-------------------------+
| plugin_dir | /usr/lib64/mysql/plugin |
+---------------+-------------------------+
1 row in set (0.00 sec)
登錄mysql,創(chuàng)建函數(shù)關(guān)聯(lián)
mysql> create function testadd returns integer soname 'test_add.so';
Query OK, 0 rows affected (0.00 sec)
至此居灯,UDF就搞定了祭务,接下來測試
mysql> select testadd(1,2);
+--------------+
| testadd(1,2) |
+--------------+
| 3 |
+--------------+
1 row in set (0.00 sec)
可以看到testadd函數(shù)生效了,輸出結(jié)果為 1 + 2 = 3.
如果要刪除UDF函數(shù)
mysql> drop function testadd;
Query OK, 0 rows affected (0.01 sec)
然后刪除插件目錄下的.so文件
[root@centos6 ~]# rm -f /usr/lib64/mysql/plugin/test_add.so
二怪嫌、結(jié)合redis做緩存更新
1.在DUF中訪問redis
更新redis的原理其實和上面示例一樣的义锥,只是要在UDF中調(diào)用redis的api函數(shù)。在此之前岩灭,請先下載api源碼
git clone https://github.com/mrpi/redis-cplusplus-client
這是redis官網(wǎng)給出的c++訪問redis的客戶端api代碼拌倍,依賴于boost,先安裝
[root@centos6 ~]# yum install boost boost-devel
然后
[root@centos6 ~]# cd redis-cplusplus-client
當(dāng)要調(diào)用該庫的時候,把如下幾個文件拷貝過去一起編譯就可以了
redisclient.h柱恤、anet.h数初、fmacros.h、anet.c梗顺。接下來看我的源碼test.cpp
#include <stdio.h>
#include <mysql.h>
#include "redisclient.h"
using namespace boost;
using namespace std;
static redis::client *_client = NULL;
// 初始化連接
void check_connection()
{
if(NULL == _client){
const char* c_host = getenv("REDIS_HOST"); // 獲取操作系統(tǒng)變量
string host = "localhost";
if(c_host)
host = c_host;
_client = new redis::client(host);
}
}
// 調(diào)用redis的hset命令
extern "C" char *redis_hset(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error)
{
try{
check_connection();
if(!(args->args && args->args[0] && args->args[1] && args->args[2])){
*is_null = 1;
*length = 8;
snprintf(result, 8, "is null");
return result;
}
if(_client->hset(args->args[0], args->args[1], args->args[2])){
*length = 2;
snprintf(result, 2, "0");
return result;
} else {
*error = 1;
*length = 5;
snprintf(result, 6, "error");
return result;
}
} catch (const redis::redis_error & e){
*length = ((std::string)e).length() + 1;
snprintf(result, *length, "%s", (char*)e.what());
return result;
}
}
/*資源分配*/
extern "C" my_bool redis_hset_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
if (3 != args->arg_count || args->arg_type[0] != STRING_RESULT || args->arg_type[1] != STRING_RESULT || args->arg_type[2] != STRING_RESULT){ // hset(key, field, value) 需要三個參數(shù)
strncpy(message, "please input 3 args and must be string, such as: hset('key', 'feild', 'value');", MYSQL_ERRMSG_SIZE);
return -1;
}
args->arg_type[0] = STRING_RESULT;
args->arg_type[1] = STRING_RESULT;
args->arg_type[2] = STRING_RESULT;
initid->ptr = NULL;
return 0;
}
/*
// 測試
int main(){
char is_null;
char message[128] = {0};
char result[128] = {0};
unsigned long length = 0;
UDF_ARGS args;
UDF_INIT initid;
args.arg_count = 3;
args.args = new char*[3];
args.args[0] = new char[16];
args.args[1] = new char[16];
args.args[2] = new char[16];
args.arg_type = new Item_result[3];
strcpy(args.args[0], "mykey");
strcpy(args.args[1], "myfeild");
strcpy(args.args[2], "myvalue");
redis_hset_init(&initid, &args, message);
redis_hset(&initid, &args, result, &length, &is_null, message);
printf("%s\n", result);
if(args.arg_type)
delete args.arg_type;
if(args.args)
delete args.args;
return 0;
}
*/
請?zhí)崆皢觬edis服務(wù)器泡孩,我是在本機(jī)啟動的,所以地址就是localhost寺谤,端口不寫默認(rèn)就是6379仑鸥。編譯和拷貝
[root@centos6 ~]# g++ -shared -fPIC -I /usr/include/mysql -lboost_serialization -o myredis.so anet.c test.cpp
[root@centos6 ~]# rm -f /usr/lib64/mysql/plugin/myredis.so && cp myredis.so /usr/lib64/mysql/plugin/ && chmod 777 /usr/lib64/mysql/plugin/myredis.so
登錄mysql客戶端,執(zhí)行
mysql> DROP FUNCTION IF EXISTS `redis_hset`; create function redis_hset returns string soname 'myredis.so';
Query OK, 0 rows affected (0.01 sec)
Query OK, 0 rows affected (0.00 sec)
mysql> select * from mysql.func;
+------------+-----+------------+----------+
| name | ret | dl | type |
+------------+-----+------------+----------+
| redis_hset | 0 | myredis.so | function |
+------------+-----+------------+----------+
安裝完成矗漾,現(xiàn)在測試锈候。在mysql中執(zhí)行
mysql> select redis_hset('Jack', 'id', '101');
+---------------------------------+
| redis_hset('User', 'id', '101') |
+---------------------------------+
| 0 |
+---------------------------------+
返回字符"0",說明調(diào)用成功敞贡。到redis上看看結(jié)果
127.0.0.1:6379> hgetall User
1) "id"
2) "101"
數(shù)據(jù)正確泵琳,mysql的DUF和redis通信成功,并且正確的修改數(shù)據(jù)誊役。
2.用觸發(fā)器實現(xiàn)動態(tài)更新redis緩存
這一步的思路获列,就是在mysql中創(chuàng)建一個觸發(fā)器,監(jiān)聽表的insert/update/delete等操作蛔垢,有數(shù)據(jù)更新的時候調(diào)用上一步的UDF函數(shù)刷新信息到redis緩存中去击孩。
先準(zhǔn)備數(shù)據(jù)庫表和觸發(fā)器
CREATE TABLE `tb_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
PRIMARY KEY (`id`)
);
觸發(fā)器
delimiter $
create trigger tg_user
after insert on tb_user
for each row
begin
set @id = (select redis_hset(CONCAT('user_', new.id), 'id', CAST(new.id AS CHAR)));
set @username = (select redis_hset(CONCAT('user_', new.id), 'username', new.username));
end $
這里觸發(fā)器監(jiān)控的是insert,當(dāng)插入新數(shù)據(jù)的時候鹏漆,會把用戶id和用戶名刷新到redis緩存中去巩梢。也可以針對update和delete做觸發(fā),篇幅有限艺玲,就不列出來了括蝠。
測試
mysql> insert into test.tb_user(`username`) values('Jack');
Query OK, 1 row affected (0.01 sec)
mysql> insert into test.tb_user(`username`) values('Lucy');
Query OK, 1 row affected (0.01 sec)
插入了兩條數(shù)據(jù),查看redis的反應(yīng)
127.0.0.1:6379> keys user_*
1) "user_1"
2) "user_2"
127.0.0.1:6379> hgetall user_1
1) "id"
2) "1"
3) "username"
4) "Jack"
127.0.0.1:6379> hgetall user_2
1) "id"
2) "2"
3) "username"
4) "Lucy"
可以看到user_1和user_2兩條key都插入了饭聚,每條key的內(nèi)容也和我們在mysql中插入的一致忌警。至此,我們的目的秒梳,“數(shù)據(jù)庫更新法绵,自動刷新到緩存”就實現(xiàn)了,妥妥的酪碘!
總結(jié)
以上的思路我已經(jīng)講的很清楚了朋譬,我只寫了一個簡單的示例,使用的是redis的hset命令兴垦。這里我們找一個現(xiàn)成的同步mysql到redis的工具此熬,很全面。從 http://pan.baidu.com/s/1qW9DHYc 下載mysql_udf_redis.tar.bz2,基本滿足常用的redis操作命令犀忱。
創(chuàng)建于 2016-08-23 杭州,更新于 2016-08-23 杭州
該文章在以下平臺同步