经过MySQL自动同步刷新Redis

      在服务端开发过程当中,通常会使用MySQL等关系型数据库做为最终的存储引擎,Redis其实也能够做为一种键值对型的数据库,但在一些实际场景中,特别是关系型结构并不适合使用Redis直接做为数据库。这俩家伙简直能够用“男女搭配,干活不累”来形容,搭配起来使用才能事半功倍。本篇咱们就这二者如何合理搭配以及他们之间数据如何进行同步展开。html

通常地,Redis能够用来做为MySQL的缓存层。为何MySQL最好有缓存层呢?想象一下这样的场景:在一个多人在线的游戏里,排行榜、好友关系、队列等直接关系数据的情景下,若是直接和MySQL正面交手,大量的数据请求可能会让MySQL疲惫不堪,甚至过量的请求将会击穿数据库,致使整个数据服务中断,数据库性能的瓶颈将掣肘业务的开发;那么若是经过Redis来作数据缓存,将大大减少查询数据的压力。在这种架子里,当咱们在业务层有数据查询需求时,先到Redis缓存中查询,若是查不到,再到MySQL数据库中查询,同时将查到的数据更新到Redis里;当咱们在业务层有修改插入数据需求时,直接向MySQL发起请求,同时更新Redis缓存。mysql

在上面这种架子中,有一个关键点,就是MySQL的CRUD发生后自动地更新到Redis里,这须要经过MySQL UDF来实现。具体来讲,咱们把更新Redis的逻辑放到MySQL中去作,即定义一个触发器Trigger,监听CRUD这些操做,当操做发生后,调用对应的UDF函数,远程写回Redis,因此业务逻辑只须要负责更新MySQL就好了,剩下的交给MySQL UDF去完成。git

一. 什么是UDF

UDF,是User Defined Function的缩写,用户定义函数。MySQL支持函数,也支持自定义的函数。UDF比存储方法有更高的执行效率,而且支持汇集函数。github

UDF定义了5个API:xxx_init()、xxx_deinit()、xxx()、xxx_add()、xxx_clear()。官方文档(http://dev.mysql.com/doc/refman/5.7/en/adding-udf.html)给出了这些API的说明。相关的结构体定义在mysql_com.h里,它又被mysql.h包含,使用时只需#include<mysql.h>便可。他们之间的关系和执行顺序能够如下图来表示:redis

1. xxx()

这是主函数,5个函数至少须要xxx(),对MySQL操做的结果在此返回。函数的声明以下:sql

char *xxx(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error);数据库

long long xxx(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error);缓存

double xxx(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error);函数

SQL的类型和C/C++类型的映射:性能

SQL Type C/C++ Type
STRING char *
INTEGER long long
REAL double

2. xxx_init()

xxx()主函数的初始化,若是定义了,则用来检查传入xxx()的参数数量、类型、分配内存空间等初始化操做。函数的声明以下:

my_bool xxx_init(UDF_INIT *initid, UDF_ARGS *args, char *message);

3. xxx_deinit()

xxx()主函数的反初始化,若是定义了,则用来释放初始化时分配的内存空间。函数的声明以下:

void xxx_deinit(UDF_INIT *initid);

4. xxx_add()

在聚合UDF中反复调用,将参数加入聚合参数中。函数的声明以下:

void xxx_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null,char *error);

5. xxx_clear()

在聚合UDF中反复调用,重置聚合参数,为下一行数据的操做作准备。函数的声明以下:

void xxx_clear(UDF_INIT *initid, char *is_null, char *error);

二. UDF函数的基本使用

在此以前,须要先安装mysql的开发包:

[root@localhost zhxilin]# yum install mysql-devel -y

咱们定义一个最简单的UDF主函数:

复制代码
 1 /*simple.cpp*/
 2 #include <mysql.h>
 3 
 4 extern "C" long long simple_add(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
 5 {
 6     int a = *((long long *)args->args[0]);
 7     int b = *((long long *)args->args[1]);
 8     return a + b;
 9 }
10 
11 extern "C" my_bool simple_add_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
12 {
13     return 0;
14 }
复制代码

因为mysql提供的接口是C实现的,咱们在C++中使用时须要添加:

extern "C" { ... }

接下来编译成动态库.so:

[zhxilin@localhost mysql-redis-test]$ g++ -shared -fPIC -I /usr/include/mysql -o simple_add.so simple.cpp

-shared 表示编译和连接时使用的是全局共享的类库;

-fPIC编译器输出位置无关的目标代码,适用于动态库;

-I /usr/include/mysql 指明包含的头文件mysql.h所在的位置。

编译出simple_add.so后用root拷贝到/usr/lib64/mysql/plugin下:

[root@localhost mysql-redis-test]# cp simple_add.so /usr/lib64/mysql/plugin/

紧接着能够在MySQL中建立函数执行了。登陆MySQL,建立关联函数:

mysql> CREATE FUNCTION simple_add RETURNS INTEGER SONAME 'simple_add.so';
Query OK, 0 rows affected (0.04 sec)

测试UDF函数:

复制代码
mysql> select simple_add(10, 5);
+-------------------+
| simple_add(10, 5) |
+-------------------+
|                15 |
+-------------------+
1 row in set (0.00 sec)
复制代码

能够看到,UDF正确执行了加法。

建立UDF函数的语法是 CREATE FUNCTION xxx RETURNS [INTEGER/STRING/REAL] SONAME '[so name]';

删除UDF函数的语法是 DROP FUNCTION simple_add;

mysql> DROP FUNCTION simple_add;
Query OK, 0 rows affected (0.03 sec)

三. 在UDF中访问Redis

跟上述作法同样,只需在UDF里调用Redis提供的接口函数。Redis官方给出了Redis C++ Client (https://github.com/mrpi/redis-cplusplus-client),封装了Redis的基本操做。

源码是依赖boost,须要先安装boost:

[root@localhost dev]# yum install boost boost-devel

而后下载redis cpp client源码:

[root@localhost dev]# git clone https://github.com/mrpi/redis-cplusplus-client

使用时须要把redisclient.h、anet.h、fmacros.h、anet.c 这4个文件考到目录下,开始编写关于Redis的UDF。咱们定义了redis_hset做为主函数,链接Redis并调用hset插入哈希表,redis_hset_init做为初始化,检查参数个数和类型。

复制代码
 1 /* test.cpp */
 2 #include <stdio.h>
 3 #include <mysql.h>
 4 #include "redisclient.h"
 5 using namespace boost;
 6 using namespace std;
 7 
 8 static redis::client *m_client = NULL;
 9 
10 extern "C" char *redis_hset(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) {
11     try {
12         // 链接Redis
13         if(NULL == m_client) {
14             const char* c_host = getenv("REDIS_HOST");
15             string host = "127.0.0.1";
16             if(c_host) {
17                 host = c_host;
18             }
19             m_client = new redis::client(host);
20         }        
21 
22         if(!(args->args && args->args[0] && args->args[1] && args->args[2])) {
23             *is_null = 1;
24             return result;
25         }
26 
27         // 调用hset插入一个哈希表
28         if(m_client->hset(args->args[0], args->args[1], args->args[2])) {
29             return result;
30         } else {
31             *error = 1;
32             return result;
33         }
34     } catch (const redis::redis_error& e) {
35         return result;
36     }
37 }
38 
39 extern "C" my_bool redis_hset_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
40     if (3 != args->arg_count) {
41         // hset(key, field, value) 须要三个参数
42         strncpy(message, "Please input 3 args for: hset('key', 'field', 'value');", MYSQL_ERRMSG_SIZE);
43         return -1;
44     }
45     if (args->arg_type[0] != STRING_RESULT  || 
46         args->arg_type[1] != STRING_RESULT  || 
47         args->arg_type[2] != STRING_RESULT) { 
48         // 检查参数类型
49         strncpy(message, "Args type error: hset('key', 'field', 'value');", MYSQL_ERRMSG_SIZE);
50         return -1;
51     }
52 
53     args->arg_type[0] = STRING_RESULT;
54     args->arg_type[1] = STRING_RESULT;
55     args->arg_type[2] = STRING_RESULT;
56 
57     initid->ptr = NULL;
58     return 0;
59 }
复制代码

编译连接:

[zhxilin@localhost mysql-redis-test]$ g++ -shared -fPIC -I /usr/include/mysql -lboost_serialization -lboost_system -lboost_thread -o libmyredis.so anet.c test.cpp

编译时须要加上-lboost_serialization -lboost_system -lboost_thread, 表示须要连接三个动态库:libboost_serialization.so、libboost_system.so、libboost_thread.so,不然在运行时会报缺乏函数定义的错误。

编译出libmyredis.so以后,将其拷贝到mysql的插件目录下并提权:

[root@localhost mysql-redis-test]# cp libmyredis.so /usr/lib64/mysql/plugin/ & chmod 777 /usr/lib64/mysql/plugin/libmyredis.so 

完成以后登陆MySQL,建立关联函数测试一下:

mysql> DROP FUNCTION IF EXISTS `redis_hset`;
Query OK, 0 rows affected (0.16 sec)

mysql> CREATE FUNCTION redis_hset RETURNS STRING SONAME 'libmyredis.so';
Query OK, 0 rows affected (0.02 sec)

先删除老的UDF,注意函数名加反引号(``)。调用UDF测试,返回0,执行成功:

复制代码
mysql> SELECT redis_hset('zhxilin', 'id', '09388334');
+-----------------------------------------+
| redis_hset('zhxilin', 'id', '09388334') |
+-----------------------------------------+
| 0                                                     |
+-----------------------------------------+
1 row in set (0.00 sec)
复制代码

打开redis-cli,查看结果:

127.0.0.1:6379> HGETALL zhxilin
1) "id"
2) "09388334"

四. 经过MySQL触发器刷新Redis

 在上一节的基础上,咱们想让MySQL在增删改查的时候自动调用UDF,还须要借助MySQL触发器。触发器能够监听INSERT、UPDATE、DELETE等基本操做。在MySQL中,建立触发器的基本语法以下:

CREATE TRIGGER trigger_name
trigger_time
trigger_event ON table_name
FOR EACH ROW
trigger_statement

trigger_time表示触发时机,值为AFTERBEFORE

trigger_event表示触发的事件,值为INSERTUPDATEDELETE等;

trigger_statement表示触发器的程序体,能够是一句SQL语句或者调用UDF。

在trigger_statement中,若是有多条SQL语句,须要用BEGIN...END包含起来:

BEGIN
[statement_list]
END

因为MySQL默认的结束分隔符是分号(;),若是咱们在BEGIN...END中出现了分号,将被标记成结束,此时无法完成触发器的定义。有一个办法,能够调用DELIMITER命令来暂时修改结束分隔符,用完再改会分号便可。好比改为$:

mysql> DELIMITER $

咱们开始定义一个触发器,监听对Student表的插入操做,Student表在上一篇文章中建立的,能够查看上一篇文章。

复制代码
mysql > DELIMITER $
      > CREATE TRIGGER tg_student 
      > AFTER INSERT on Student 
      > FOR EACH ROW 
      > BEGIN
      > SET @id = (SELECT redis_hset(CONCAT('stu_', new.Sid), 'id', CAST(new.Sid AS CHAR(8))));
      > SET @name = (SELECT redis_hset(CONCAT('stu_', new.Sid), 'name', CAST(new.Sname AS CHAR(20))));
      > Set @age = (SELECT redis_hset(CONCAT('stu_', new.Sid), 'age', CAST(new.Sage AS CHAR))); 
      > Set @gender = (SELECT redis_hset(CONCAT('stu_', new.Sid), 'gender', CAST(new.Sgen AS CHAR))); 
      > Set @dept = (SELECT redis_hset(CONCAT('stu_', new.Sid), 'department', CAST(new.Sdept AS CHAR(10))));    
      > END $
复制代码

建立完触发器能够经过show查看,或者drop删除:

mysql> SHOW TRIGGERS;
mysql> DROP TRIGGER tg_student;

接下来咱们调用一句插入语句,而后观察Redis和MySQL数据的变化:

mysql> INSERT INTO Student VALUES('09388165', 'Rose', 19, 'F', 'SS3-205');
Query OK, 1 row affected (0.27 sec)

MySQL的结果:

复制代码
mysql> SELECT * FROM Student;
+----------+---------+------+------+---------+
| Sid      | Sname   | Sage | Sgen | Sdept   |
+----------+---------+------+------+---------+
| 09388123 | Lucy    |   18 | F    | AS2-123 |
| 09388165 | Rose    |   19 | F    | SS3-205 |
| 09388308 | zhsuiy  |   19 | F    | MD8-208 |
| 09388318 | daemon  |   18 | M    | ZS4-630 |
| 09388321 | David   |   20 | M    | ZS4-731 |
| 09388334 | zhxilin |   20 | M    | ZS4-722 |
+----------+---------+------+------+---------+
6 rows in set (0.00 sec)
复制代码

Redis的结果:

复制代码
127.0.0.1:6379> HGETALL stu_09388165
 1) "id"
 2) "09388165"
 3) "name"
 4) "Rose"
 5) "age"
 6) "19"
 7) "gender"
 8) "F"
 9) "department"
10) "SS3-205"
复制代码

以上结果代表,当MySQL插入数据时,经过触发器调用UDF,实现了自动刷新Redis的数据。另外,调用MySQL插入的命令,能够经过C++实现,进而就实现了在C++的业务逻辑里,只需调用MySQL++的接口就能实现MySQL数据库和Redis缓存的更新。