MySQL与Redis的数据同步方案

之前的文章有讲过MySQL到Elasticsearch的多种数据同步方案(多种MySQL与Elasticsearch的数据同步解决方案),今天再来讲下MySQL到Redis的几种数据同步方案。

第一种方案:使用 canal 工具

在之前MySQL同步ES的文章中有简单提过这款工具,但是因为没有用过所以没有详细讲,今天就使用这款工具来实现MySQL到Redis的数据同步(同理,同步到ES、MySQL等也是一样的操作)

canal 是阿里巴巴开源的一款提供增量数据订阅和消费的工具,应用场景有:数据库镜像、数据库实时备份、索引构建和实时维护、业务 cache 刷新、带业务逻辑的增量数据处理等。

原理就与MySQL主从复制相似,canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议,从binlog日志中获取数据。

  1. MySQL配置

    MySQL需要开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式

    [mysqld]
    log-bin=mysql-bin # 开启 binlog
    binlog-format=ROW # 选择 ROW 模式
    server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
    

    授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant

    CREATE USER canal IDENTIFIED BY 'canal';  
    GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
    FLUSH PRIVILEGES;
    
  2. 下载 canal

    下载地址,选择自己需要的版本:https://github.com/alibaba/canal/releases

    wget https://github.com/alibaba/canal/releases/download/canal-1.1.5/canal.deployer-1.1.5.tar.gz
    mkdir /usr/local/canal
    tar -zxvf canal.deployer-1.1.5.tar.gz -C /usr/local/canal/
    rm -rf canal.deployer-1.1.5.tar.gz
    cd /usr/local/canal/
    
  3. 修改配置

    vi conf/example/instance.properties

    ## mysql serverId 不能与mysql的server-id一致
    canal.instance.mysql.slaveId = 1234
    #position info,需要改成自己的数据库信息
    canal.instance.master.address = 127.0.0.1:3306 
    canal.instance.master.journal.name = 
    canal.instance.master.position = 
    canal.instance.master.timestamp = 
    #canal.instance.standby.address = 
    #canal.instance.standby.journal.name =
    #canal.instance.standby.position = 
    #canal.instance.standby.timestamp = 
    #username/password,需要改成自己的数据库信息
    canal.instance.dbUsername = canal  
    canal.instance.dbPassword = canal
    canal.instance.defaultDatabaseName =
    canal.instance.connectionCharset = UTF-8
    #table regex
    canal.instance.filter.regex = .\*\\\\..\*
    
  4. 安装java的JDK

    # 查看可安装jdk版本
    yum search java | grep -i --color JDK
    # 选择某一版本进行安装
    yum install java-1.8.0-openjdk.x86_64
    # 安装完成后确认JDK安装完毕,如果输出了版本号,证明安装正确
    java -version
    

    PS:这里有个坑,要安装 JDK8,我用的 JDK11的环境,发现启动不了canal,报错 Error: Could not create the Java Virtual Machine. ,切换成 JDK8 就好了

  5. 启动

    sh bin/startup.sh
    
  6. 检查

    查看 server 日志 和 instance 的日志,有正确的内容输出证明启动成功

    vi logs/canal/canal.log
    vi logs/example/example.log
    

    或者使用 ps -ef | grep canal 查看canal进程

  7. 关闭命令

    sh bin/stop.sh
    
  8. 安装 canal-php

    canal 提供了多语言的客户端,可采用不同语言实现不同的消费逻辑,我用的PHP客户端,它的详细介绍以及其他语言客户端看文档:https://github.com/alibaba/canal/wiki#%E5%A4%9A%E8%AF%AD%E8%A8%80

    composer require xingwenge/canal_php
    

    我这通过创建一个命令来测试

    php artisan make:command canal
    

    内容

    connect("127.0.0.1", 11111);
                $client->checkValid();
                // $client->subscribe("1001", "example", ".*\\..*");
                // 设置过滤,指定要同步的表,上边那种方式是不限制
                $client->subscribe("1001", "example", "lmrs.lmrs_shops");
    
                while (true) {
                    $message = $client->get(100);
                    if ($entries = $message->getEntries()) {
                        foreach ($entries as $entry) {
                            // Fmt::println($entry);
                            // 在这里进行具体业务的逻辑处理,比如同步数据到 redis,es,mysql等
                            CanalToRedisService::println($entry);
                        }
                    }
                    sleep(1);
                }
    
                $client->disConnect();
            } catch (\Exception $e) {
                echo $e->getMessage(), PHP_EOL;
            }
        }
    }
    

    这里我创建了个service来处理同步到Redis的逻辑,内容如下

    getEntryType()) {
                case EntryType::TRANSACTIONBEGIN:
                case EntryType::TRANSACTIONEND:
                    return;
                    break;
            }
    
            $rowChange = new RowChange();
            $rowChange->mergeFromString($entry->getStoreValue());
            $evenType = $rowChange->getEventType();
            $header = $entry->getHeader();
            $table = $header->getSchemaName().'_'.$header->getTableName();
    
            /** @var RowData $rowData */
            foreach ($rowChange->getRowDatas() as $rowData) {
                switch ($evenType) {
                    case EventType::DELETE: // 删除
                        self::delete($table, self::ptColumn($rowData->getBeforeColumns()));
                        break;
                    case EventType::INSERT: // 新增
                        self::insert($table, self::ptColumn($rowData->getAfterColumns()));
                        break;
                    default: // 更新
                        self::update($table, self::ptColumn($rowData->getBeforeColumns()), self::ptColumn($rowData->getAfterColumns()));
                        break;
                }
            }
        }
    
        /**
         * 将数据表的字段名和值组装成数组
         * @param $columns
         * @return array
         */
        private static function ptColumn($columns) {
            $argv = [];
            foreach ($columns as $value) {
                $argv[$value->getName()] = $value->getValue();
            }
         // dump($argv);
            return $argv;
        }
    
        /**
         * 新增操作
         * 可以根据表名进行判断具体的业务操作
         * @param string $table 数据表名
         * @param array $data 数据
         */
        private static function insert($table, $data)
        {
            app('redis')->set("shop::".$data['id'], serialize($data));
        }
    
        /**
         * 删除操作
         * 业务处理很简单,这里就不写了,自己完善
        *  @param string $table 数据表名
         * @param array $data 数据
         */
        private static function delete($table, $data)
        {
            //
        }
    
        /**
         * 更新操作
         * 业务处理很简单,这里就不写了,自己完善
         * @param string $table 数据表名
         * @param array $befor_data 更改前的数据
         * @param array $after_data 更改后的数据
         */
        private static function update($table, $befor_data, $after_data)
        {
         //
        }
    }
    
  9. 测试

    可以在service里多dump一些参数,运行 php artisan canal 查看输出,新增MySQL数据,查看Redis是否有变化

  10. canal 还可以结合消息中间件来实现更高效的数据同步,比如:Kafka/RocketMQ 。使用文档:https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart

第二种方案:使用 RabbitMQ 消息队列

RabbitMQ 是一个由erlang语言编写的、开源的、在AMQP基础上完整的、可复用的企业消息系统。支持多种语言,包括java、Python、ruby、PHP、C/C++等。

RabbitMQ 的核心概念:

  • 生产者(Producer):发送消息的应用
  • 消费者(Consumer):接收消息的应用
  • 队列(Queue):存储消息的缓存
  • 消息(Message):由生产者通过RabbitMQ发送给消费者的信息
  • 连接(Connection):连接RabbitMQ和应用服务器的TCP连接
  • 通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的
  • 交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机
  • 绑定(Binding):绑定是队列和交换机的一个关联连接
  • 路由键(Routing Key):路由键是供交换机查看并根据键来决定如何分发消息到列队的一个键。路由键可以说是消息的目的地址

RabbitMQ 的工作模式:

  • 简单队列
  • 工作队列
  • 发布订阅模式
  • 路由模式
  • 主题模式

理论介绍完毕,接下来进入实操

  1. 安装 RabbitMQ

    手动编译安装 RabbitMQ 很麻烦,还得先安装 erlang 环境,所以这里我就直接使用docker安装了。附上erlang和RabbitMQ的下载地址,之后有时间再去尝试手动安装

    erlang:https://www.erlang.org/downloads

    RabbitMQ:https://github.com/rabbitmq/rabbitmq-server/releases/

    拉取docker镜像

    docker pull rabbitmq
    

    构建容器

    docker run -d -p 5672:5672 -p 15672:15672 --hostname my-rabbit -v /docker/rabbitmq:/var/lib/rabbitmq --privileged=true --name rabbitmq rabbitmq
    
  2. 进入RabbitMQ 容器安装可视化界面:rabbitmq_management

    docker exec -it rabbitmq bash
    
    rabbitmq-plugins enable rabbitmq_management
    

    在浏览器访问 ip:15672 打开可视化界面,账号密码默认都是:guest

  3. 安装扩展

    PHP调用RabbitMQ需要amqp的扩展,下载地址:https://pecl.php.net/package/amqp

    wget https://pecl.php.net/get/amqp-1.10.2.tgz
    tar -zxvf amqp-1.10.2.tgz
    cd amqp-1.10.2
    phpize
    ./configure --with-php-config=/usr/local/bin/php-config
    

    到这里报了一个错,configure: error: librabbitmq not found 意思是还缺少个rabbitmq-c

    接着下载,地址:https://github.com/alanxz/rabbitmq-c/releases

    wget https://github.com/alanxz/rabbitmq-c/archive/refs/tags/v0.11.0.tar.gz
    tar -zxvf v0.11.0.tar.gz
    cd rabbitmq-c-0.11.0/
    yum -y install cmake
    cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/rabbitmq-c
    make && make install
    

    重新编译amqp

    cd amqp-1.10.2
    ./configure --with-php-config=/usr/local/bin/php-config --with-amqp --with-librabbitmq-dir=/usr/local/rabbitmq-c
    make && make install
    

    在 php.ini 中加入 extension=amqp.so ,然后重启php

  4. rabbimq在laravel中使用

    安装组件

    composer require vladimir-yuldashev/laravel-queue-rabbitmq "10.X" --ignore-platform-reqs
    

    在 config/queue.php文件的 connections 中加入配置

    'rabbitmq' => [
    
       'driver' => 'rabbitmq',
       'queue' => env('RABBITMQ_QUEUE', 'default'),
       'connection' => PhpAmqpLib\Connection\AMQPLazyConnection::class,
    
       'hosts' => [
           [
               'host' => env('RABBITMQ_HOST', '127.0.0.1'),
               'port' => env('RABBITMQ_PORT', 5672),
               'user' => env('RABBITMQ_USER', 'guest'),
               'password' => env('RABBITMQ_PASSWORD', 'guest'),
               'vhost' => env('RABBITMQ_VHOST', '/'),
           ],
       ],
    
       'options' => [
           'ssl_options' => [
               'cafile' => env('RABBITMQ_SSL_CAFILE', null),
               'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null),
               'local_key' => env('RABBITMQ_SSL_LOCALKEY', null),
               'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true),
               'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null),
           ],
           'queue' => [
               'job' => VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Jobs\RabbitMQJob::class,
           ],
       ],
    
       /*
        * Set to "horizon" if you wish to use Laravel Horizon.
        */
       'worker' => env('RABBITMQ_WORKER', 'default'),
        
    ],
    

    在 .env 文件中加入配置

    # 将默认的 sync 改为 rabbitmq                            
    QUEUE_CONNECTION=rabbitmq
    # mq的ip地址      
    RABBITMQ_HOST=172.17.0.10
    # mq的端口 
    RABBITMQ_PORT=5672
    # mq的账号 
    RABBITMQ_USER=guest
    # mq的密码
    RABBITMQ_PASSWORD=guest
    # 默认的虚拟主机 
    RABBITMQ_VHOST=my_vhost
    # 默认队列名称 
    RABBITMQ_QUEUE=lmrs
    
  5. 创建 service

     env('RABBITMQ_HOST', '127.0.0.1'),
                'port' => env('RABBITMQ_PORT', 5672),
                'user' => env('RABBITMQ_USER', 'guest'),
                'password' => env('RABBITMQ_PASSWORD', 'guest'),
                'vhost' => env('RABBITMQ_VHOST', '/'),
            ];
            return new AMQPStreamConnection($config["host"],$config["port"],$config["user"],$config["password"],$config["vhost"]);
        }
    
        /**
         * 生产者
         * @param $queue
         * @param $messageBody
         * @param string $exchange
         */
        public static function push($queue,$messageBody,$exchange='router')
        {
            //获取连接
            $connection = self::getConnect();
            //构建通道
            $channel = $connection->channel();
            //声明一个队列
            $channel->queue_declare($queue,false,true,false,false);
            //指定交换机 以路由模式
            $channel->exchange_declare($exchange,'direct',false,true,false);
            //绑定队列和类型
            $channel->queue_bind($queue,$exchange);
            $message = new AMQPMessage($messageBody,array('content_type' => 'text/plain',
                'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT));
            //消息推送
            $channel->basic_publish($message,$exchange);
            $channel->close();
            $connection->close();
        }
    
        /**
         * 消费者
         * @param $queue
         * @param $callback
         * @param string $exchange
         */
        public static function pop($queue,$callback,$exchange='router')
        {
            $connection = self::getConnect();
            $channel = $connection->channel();
            //从队列中取出消息
            $message = $channel->basic_get($queue);
            $res = $callback($message->getBody());
            if ($res){
                //ack 验证
                $channel->basic_ack($message->getDeliveryTag());
            }
            $channel->close();
            $connection->close();
        }
    }
    
  6. 创建异步任务

    php artisan make:job SyncToRedis
    

    编辑内容

    key = 'lmrs::product::info::'.$data->id;
            //写入队列
            RabbitmqService::push('update_queue', $data);
        }
    
        /**
         * Execute the job.
         *
         * @return void
         */
        public function handle()
        {
            // 消费消息
            RabbitmqService::pop('update_queue', function ($message) {
                $product = app('redis')->set($this->key, serialize($message));
                if (!$product){
                    return;
                }
    
                return true;
            });
        }
    
        /**
         * 异常处理
         * @param \Exception $exception
         */
        public function failed(\Exception $exception)
        {
            print_r($exception->getMessage());
        }
    }
    
  7. 在需要同步的地方触发任务

    dispatch(new SyncToRedis(Product::find($request->input("id"))));
    

你可能感兴趣的:(MySQL与Redis的数据同步方案)