通过UDF使mysql主动刷新redis缓存

UDF是mysql的一个拓展接口,UDF(Userdefined function)用户自定义函数。在什么地方使用这个功能呢,试想有如下场景:

你的网站使用mysql作为最终数据落地的存储引擎,而redis作为缓存以减小查询请求穿透到mysql的数量,可以极大的降低数据库性能瓶颈带来的整个网站对外服务的卡顿、不可用等情况。这种方式的架构,当有查询请求的时候,我们可以在业务逻辑层控制,先从缓存中查询,无命中的情况下,再到数据库中查询,同时缓存到redis中;当有修改请求的时候,我们可以先修改数据库,然后删除或更新缓存。

以上方式是我们业务量不大,开发简单的,少横向扩展的情况下做的。当开发复杂度随着业务量并发增大,呈现横向扩展和垂直方向上螺旋迭代上升趋势的时候,逻辑复杂度直线上升。还采用在业务逻辑层做缓存控制将变得很复杂,运维上也容易出错。

这个时候,如果能将缓存逻辑和业务逻辑分离,缓存层对业务逻辑提供服务透明,业务逻辑不用关心缓存逻辑,缓存逻辑也不用随业务变化而改动,互相做自己的事情,这样高内聚低耦合可以极大的增加扩展性和健壮性,也是我们做架构应该努力发展的方向。

那么具体来说,我的这里要做的事情,其实就是把缓存更新的逻辑,放到mysql中去做。写一个trigger触发器监控insert/update/delete这些修改数据的操作,当有修改操作的时候,调用对应的自定义UDF函数来远程回写redis缓存,而我们在业务逻辑层则只管更新数据就行了,缓存更新的操作都放给以上的缓存层逻辑来完成。

当然,以上操作也可以反方向来,先写redis,然后由redis同步到mysql去。两种方式各有利弊,看你的具体场景如何选择了,这里不讨论。我们此处的目的是使用mysql的udf函数更新数据到redis中。

开发环境

操作系统:centos 6.4 server,内核2.6.32-358.18.1.el6.x86_64

编译器:gcc 4.4.7

数据库:mysql server 5.1.73

mysql服务器之前已经安装了,现在我们要安装mysql开发包

yum install mysql-devel -y

一、UDF函数入门

首先学习下UDF函数的使用方法。我们自定义一个函数文件,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/

如果你不知道插件的路径,执行

mysql> show variables like '%plugin%';
+---------------+-------------------------+
| Variable_name | Value                   |
+---------------+-------------------------+
| plugin_dir    | /usr/lib64/mysql/plugin |
+---------------+-------------------------+
1 row in set (0.00 sec)

登录mysql,创建函数关联

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函数生效了,输出结果为 1 + 2 = 3.

如果要删除UDF函数

mysql> drop function testadd;
Query OK, 0 rows affected (0.01 sec)

然后删除插件目录下的.so文件

[root@centos6 ~]# rm -f /usr/lib64/mysql/plugin/test_add.so

二、结合redis做缓存更新

1.在DUF中访问redis

更新redis的原理其实和上面示例一样的,只是要在UDF中调用redis的api函数。在此之前,请先下载api源码
git clone https://github.com/mrpi/redis-cplusplus-client
这是redis官网给出的c++访问redis的客户端api代码,依赖于boost,先安装

[root@centos6 ~]# yum install boost boost-devel

然后

[root@centos6 ~]# cd redis-cplusplus-client

当要调用该库的时候,把如下几个文件拷贝过去一起编译就可以了
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"); // 获取操作系统变量
        string host = "localhost";
        if(c_host)
            host = c_host;
        _client = new redis::client(host);
    }
}

// 调用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) 需要三个参数
        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;
}
*/

请提前启动redis服务器,我是在本机启动的,所以地址就是localhost,端口不写默认就是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客户端,执行

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 |
+------------+-----+------------+----------+

安装完成,现在测试。在mysql中执行

mysql> select redis_hset('Jack', 'id', '101');
+---------------------------------+
| redis_hset('User', 'id', '101') |
+---------------------------------+
| 0                               |
+---------------------------------+

返回字符"0",说明调用成功。到redis上看看结果

127.0.0.1:6379> hgetall User
1) "id"
2) "101"

数据正确,mysql的DUF和redis通信成功,并且正确的修改数据。

2.用触发器实现动态更新redis缓存

这一步的思路,就是在mysql中创建一个触发器,监听表的insert/update/delete等操作,有数据更新的时候调用上一步的UDF函数刷新信息到redis缓存中去。

先准备数据库表和触发器

CREATE TABLE `tb_user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
  PRIMARY KEY (`id`)
);

触发器

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 $

这里触发器监控的是insert,当插入新数据的时候,会把用户id和用户名刷新到redis缓存中去。也可以针对update和delete做触发,篇幅有限,就不列出来了。

测试

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)

插入了两条数据,查看redis的反应

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的内容也和我们在mysql中插入的一致。至此,我们的目的,“数据库更新,自动刷新到缓存”就实现了,妥妥的!

总结

以上的思路我已经讲的很清楚了,我只写了一个简单的示例,使用的是redis的hset命令。这里我们找一个现成的同步mysql到redis的工具,很全面。从 http://pan.baidu.com/s/1qW9DHYc 下载mysql_udf_redis.tar.bz2,基本满足常用的redis操作命令。


创建于 2016-08-23 杭州,更新于 2016-08-23 杭州

该文章在以下平台同步

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,050评论 18 139
  • 转载地址:http://gnucto.blog.51cto.com/3391516/998509 Redis与Me...
    Ddaidai阅读 21,384评论 0 82
  • 道艺合一:追寻生命根本意义,实践生活美学之 在自己的宁静中击鼓 ——台湾艺文团体“优人神鼓” 优,在中国传统戏曲中...
    海蓝堡堡主阅读 368评论 0 18
  • 因为各种原因今天工作效率很低,明天也被安排了。 要做合群的事,然后坚持独行的信念。
    武允儿阅读 268评论 0 0
  • 我素来是个好静的人,喜欢独处,喜欢大自然。 走在乡间的田野里,很多时候,四周一片寂静。天地间仿佛只有我一人。 我家...
    一世福缘阅读 2,430评论 155 211