In this blog article I’ll demonstrate a proof of concept: MySQL replication to a Redis cache server. To outline the components used:
Ubuntu - linux
Mysql - primary database
Redis - cache database
Ruby - Gearman worker + client
Gearman - job queue service
MySQL JSON UDF
MySQL Gearman UDF
At Ziftr we implemented a similar configuration to produce high volume streams of data for ziftr.com .
I started with a clean and minimal installation of Ubuntu 12.10 Server.
# upgrade packages
apt-get update
apt-get upgrade -y
# install ssh server
apt-get install openssh-server -y
Installed Mysql Server
apt-get install mysql-server -y
Install Ruby via RVM
curl -L https://get.rvm.io | bash -s stable
source /etc/profile.d/rvm.sh
# rvm requirements:
apt-get --no-install-recommends install build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev libgdbm-dev ncurses-dev automake libtool bison subversion pkg-config libffi-dev
rvm install ruby
Install Redis
apt-get install tcl8.5 -y
wget http://redis.googlecode.com/files/redis-2.6.9.tar.gz
tar -xzf redis-2.6.9.tar.gz
cd redis-2.6.9
make
make test
cd src
cp redis-benchmark redis-check-aof redis-check-dump redis-cli redis-sentinel redis-server /usr/local/bin/
cd ..
cp redis.conf /etc
# start redis
redis-server /etc/redis.conf &
# test redis
redis-cli ping
PONG
Install Gearman
apt-get install gearman gearman-server -y
# check if service is running
/etc/init.d/gearman-job-server status
* gearmand is running
Installed MySQL JSON UDF
apt-get install libmysqlclient-dev -y
cd
mkdir ~/lib_mysqludf_json
cd ~/lib_mysqludf_json
wget http://www.mysqludf.org/lib_mysqludf_json/lib_mysqludf_json_0.0.2.tar.gz
tar -xzf lib_mysqludf_json_0.0.2.tar.gz
# remove shared object, and recompile
rm lib_mysqludf_json.so
gcc $( mysql_config --cflags ) -shared -fPIC -o lib_mysqludf_json.so lib_mysqludf_json.c
# locate plugin directory
mysql -u root -pPASSWORD --execute = "show variables like '%plugin%';"
+---------------+------------------------+
| Variable_name | Value |
+---------------+------------------------+
| plugin_dir | /usr/lib/mysql/plugin/ |
+---------------+------------------------+
# copy shared object to plugin directory
cp lib_mysqludf_json.so /usr/lib/mysql/plugin/
# enable json_object method
mysql -u root -pPASSWORD --execute = "create function json_object returns string soname 'lib_mysqludf_json.so'"
Installed MySQL Gearman UDF
apt-get install libgearman-dev -y
cd
wget https://launchpad.net/gearman-mysql-udf/trunk/0.6/+download/gearman-mysql-udf-0.6.tar.gz
tar -xzf gearman-mysql-udf-0.6.tar.gz
cd gearman-mysql-udf-0.6
./configure --with-mysql = /usr/bin/mysql_config --libdir = /usr/lib/mysql/plugin/
make
make install
# enabled udf functions
mysql -u root -pPASSWORD --execute = "CREATE FUNCTION gman_do_background RETURNS STRING SONAME 'libgearman_mysql_udf.so'"
mysql -u root -pPASSWORD --execute = "CREATE FUNCTION gman_servers_set RETURNS STRING SONAME 'libgearman_mysql_udf.so'"
# set gearman server
mysql -u root -pPASSWORD --execute = "SELECT gman_servers_set('127.0.0.1')"
Ruby setup code
# setup RVM gemset
mkdir ~/ruby
echo "rvm use --create ruby-1.9.3@redis_gearman" > ~/ruby/.rvmrc
cd ~/ruby
Created new file: Gemfile; contents:
source 'https://rubygems.org'
gem 'gearman-ruby'
gem 'redis'
Execute bundle to install gems:
bundle
Created redis gearman worker, new file: redis_worker.rb
#!/usr/bin/env ruby
require 'rubygems'
require 'gearman'
require 'redis'
require 'json'
servers = [ 'localhost' ]
worker = Gearman :: Worker . new ( servers )
REDIS_DELIMITER = ':'
$redis = Redis . new
module RedisWorker
def RedisWorker . work ( data , job )
# decode json
json_data = JSON . parse data
# create redis key
redis_key = "user_page_views #{ REDIS_DELIMITER }#{ json_data [ 'user_id' ] } "
$redis . lpush redis_key , data
true
end
end
worker . add_ability ( 'redis_worker' ) do | data , job |
RedisWorker :: work data , job
end
loop { worker . work }
Set file executable
chmod +x redis_worker.rb
Created redis gearman [test] client, new file: redis_client.rb
#!/usr/bin/env ruby
require 'rubygems'
require 'gearman'
require 'json'
servers = [ 'localhost' ]
client = Gearman :: Client . new ( servers )
taskset = Gearman :: TaskSet . new ( client )
data = '{"user_id":1,"timestamp":"2013-02-14 19:13:15","page":"http://www.google.com"}'
result = client . do_task ( 'redis_worker' , data )
puts result
Set file executable
chmod +x redis_client.rb
Testing Gearman worker & Redis
# in terminal 1, start worker
./redis_worker.rb
# in terminal 2, check gearman status & verify worker
( echo status ; sleep 0.1) | netcat 127.0.0.1 4730
redis_worker 0 0 1
# in terminal 3, monitor redis
redis-cli monitor
OK
# in terminal 4, run client test script
./redis_client.rb
true
# in terminal 3, verify redis lpush:
redis-cli monitor
OK
1361012555.700504 [ 0 127.0.0.1:34135] "lpush" "user_page_views:1" "{ \" user_id \" :1, \" timestamp \" : \" 2013-02-14 19:13:15 \" , \" page \" : \" http://www.google.com \" }"
MySQL data setup
# add database & table
mysql -u root -pPASSWORD
mysql> create database redisgearman;
mysql> use redisgearman;
mysql> CREATE TABLE ` user_page_views` (
` id ` int( 11) unsigned NOT NULL AUTO_INCREMENT,
` user_id` int( 11) NOT NULL,
` timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
` page` varchar( 255) NOT NULL DEFAULT '' ,
PRIMARY KEY ( ` id ` ) ,
KEY ` user_id` ( ` user_id` )
) ENGINE = InnoDB DEFAULT CHARSET = latin1;
mysql> exit
# add test record
mysql -u root -pPASSWORD redisgearman --execute = "insert into user_page_views (user_id, page) values (1, 'http://www.google.com')"
# ensure json udf is working
mysql -u root -pPASSWORD redisgearman --execute = "select json_object(user_id as \` user_id \` , timestamp as \` timestamp \` , page as \` page \` ) as json from user_page_views"
+--------------------------------------------------------------------------------+
| json |
+--------------------------------------------------------------------------------+
| { "user_id" :1,"timestamp" :"2013-02-14 19:13:15" ,"page" :"http://www.google.com" } |
+--------------------------------------------------------------------------------+
Add MySQL trigger, new file: ~/trigger.sql
DELIMITER $$
CREATE TRIGGER redisgearman . redis_gearman AFTER INSERT ON redisgearman . user_page_views
FOR EACH ROW BEGIN
SET @ ret = gman_do_background ( 'redis_worker' , json_object ( NEW . user_id as `user_id` , NEW . timestamp as `timestamp` , NEW . page as `page` ));
END $$
DELIMITER ;
Enable trigger
mysql -u root -pPASSWORD redisgearman < ~/trigger.sql
Trigger + Gearman worker + Redis test.
# example insert statement
insert into user_page_views ( user_id, page) values ( 1, 'http://ericlondon.com/recent-posts' ) ;
# mysql insert => mysql trigger => gearman udf => ruby redis worker => redis insert..
# output from redis-cli monitor
redis-cli monitor
OK
1361327500.888805 [ 0 127.0.0.1:33649] "lpush" "user_page_views:1" "{ \" user_id \" :1, \" timestamp \" : \" 2013-02-19 21:31:40 \" , \" page \" : \" http://ericlondon.com/recent-posts \" }"
Now the MySQL table replicates to the Redis cache database. For future queries, check Redis first, and fall back on MySQL. Here’s an example class to do so:
#!/usr/bin/env ruby
require 'rubygems'
require 'redis'
require 'json'
require 'mysql2'
class RedisMysql
def initialize
@redis = Redis . new
@mysql = Mysql2 :: Client . new ( :host => 'localhost' , :username => 'root' , :password => 'PASSWORD' , :database => 'redisgearman' )
@redis_results = []
end
def query ( key , limit )
@redis_results = query_redis key , limit
return @redis_results if @redis_results . size >= limit
@mysql_results = query_mysql key , ( limit - @redis_results . size )
@redis_results . concat @mysql_results
end
def query_redis ( key , limit )
results = @redis . lrange key , 0 , limit
return [] if results . nil?
results . collect { | r | JSON . parse r }
end
def query_mysql ( key , limit )
# parse args
parts = key . split ':'
mysql_table = parts [ 0 ]
user_id = parts [ 1 ]
# get last timestamp from redis results
last_timestamp = @redis_results . last [ 'timestamp' ] unless @redis_results . empty?
where = []
where << "user_id = ' #{ @mysql . escape user_id } '"
where << "timestamp < ' #{ @mysql . escape last_timestamp } '" unless last_timestamp . nil?
sql = "
select *
from user_page_views
where #{ where . join ' and ' }
order by id desc
limit #{ limit } "
results = @mysql . query sql
return [] if results . nil?
results . collect { | r | r }
end
end
Class usage
rm = RedisMysql . new
results = rm . query 'user_page_views:1' , 10
# debug
puts results
Source code on GitHub