一、數(shù)據(jù)類型
redis支持以下五種數(shù)據(jù)類型:
1. String類型
二進制安全,可存儲字符串、圖片和視頻等,支持incr自增操作(set/get/mset/mget/incr...)
2. List類型
雙向鏈表,可以實現(xiàn)消息隊列功能(rpush/lpop/llen...)
3. Set類型
無序集合,通過HashTable實現(xiàn),可實現(xiàn)快速查找(去重)(sadd/srem/sismember...)
4. Sorted Set類型
有序集合,排好序的Set類型,可以用來實現(xiàn)優(yōu)先級隊列(zadd/zrem/zincrby...)
5. Hash類型
每個key都是一個HashTable,適合存儲對象,如用戶信息對象,id作key(hset/hget/hmset/hmget/hlen...)
二、事務處理
redis支持簡單的事務操作,通過multi命令進入事務操作的上下文,接下來的redis命令將依次存入隊列,識別到exec命令時,redis將按隊列順序執(zhí)行隊列中的所有命令,并將執(zhí)行結果一并打包返回,然后終結事務上下文。
三、持久化
redis是基于內存的數(shù)據(jù)庫,內存數(shù)據(jù)存在一個嚴重的弊端:突然宕機后者斷電時,內存的數(shù)據(jù)不會保存。為了解決這個問題,redis提供了2種持久化的方式,分別為內存快照(Snapshotting)和日志追加(Append-only file),下面介紹一下。
1. 內存快照(RDB)
內存快照是將內存中的數(shù)據(jù)寫入二進制文件中,默認文件名為dump.rdb。redis重啟時讀取rdb中的數(shù)據(jù)。
redis內存快照分為自動和手動2種觸發(fā)模式:
1.1 手動觸發(fā)
客戶端使用save或bgsave命令告訴redis需要做一次快照操作
- save:阻塞redis服務器,拒絕所有redis命令,直到save完成。 PS: 盡量不要使用
- bgsave:fork一個子進程(非線程),通過子進程去進行save操作,而主進程可以執(zhí)行命令。
需要注意以下問題:
- 在執(zhí)行bgsave時,為避免父子進程同時執(zhí)行rdbSave而產生競爭,客戶端發(fā)送的save命令將被拒絕。
- 在執(zhí)行bgsave時,如果發(fā)送了bgrewriteaof(aof重寫)命令,該命令將延遲到bgsave完成之后執(zhí)行,如果正在執(zhí)行bgrewriteaof命令,bgsave將被拒絕。
- 雖然bgsave是由子進程進行rdb文件的生成,但是在fork子進程的時候依然會造成父進程的阻塞,因此使用時要格外注意。
1.2 自動觸發(fā)
因為basave命令可以不阻塞父進程保存數(shù)據(jù),所以redis可以設置服務器配置的save選項,讓服務器每隔一段時間自動執(zhí)行一次bgsave命令。
save 秒數(shù) 修改次數(shù)
可以設置多個條件實現(xiàn)不同的快照方案,滿足任何一個條件,redis都將進行一次內存快照:
save 900 1
save 300 10
save 60 1000
2. 日志追加(AOF)
aof是把增加、修改的命令通過write函數(shù)追加到日志文件的尾部(默認名appendonly.aof)。redis重啟時讀取aof文件中的所有命令并且執(zhí)行,從而把數(shù)據(jù)寫入內存中。
由于操作系統(tǒng)內核的I/O接口可能存在緩存,所以日志追加的方式可能不會立即寫入文件中,這就有可能丟失部分數(shù)據(jù)。redis可以提供了以下三種方式配置來告訴redis何時去執(zhí)行fsync函數(shù)強制系統(tǒng)將緩存寫入磁盤:
#appendfsync always # 每次收到增加或修改命令就立刻強制寫入磁盤
appendfsync everysec # 每秒強制寫入磁盤一次
#appendfsync no # 是否寫入磁盤完全依賴系統(tǒng)
日志追加的方式有效降低了數(shù)據(jù)丟失的風險,也帶了另一個問題,那就是持久化文件不斷膨脹。例如,執(zhí)行incr nums命令100次,文件就會保存100條incr命令,其實99條都是多余的命令,因為恢復數(shù)據(jù)只需要執(zhí)行set nums 100就可以了。
redis提供了bgwriteaof,當redis收到此命令時,就使用類似于內存快照的方式將內存的數(shù)據(jù)以命令的方式保存到臨時文件中,最后替換原來的日志文件。
四、主從同步
redis支持主從同步。主從同步可以防止主機(Master)掛掉導致網站不能正常運作,只需要把從機(Slave)設置為主機即可。主從同步有諸多優(yōu)點:
- Master可以有多個Slave。
- 多個Slave可以連接到相同的Master,還可以連接到其他Slave形成圖形結構。
- 不會阻塞Master。當一個或多個Slave與Master初次進行數(shù)據(jù)同步時,Master可以繼續(xù)處理客戶端請求。相反,Slave在初次進行數(shù)據(jù)同步時不能處理請求(2.2版本以后不再阻塞)。
- 在Master服務器禁止持久化,只在Slave服務器進行數(shù)據(jù)持久化。
1. 主從同步原理
主從同步設置好以后,Slave自動與Master建立連接,發(fā)送SYNC命令。無論是初次建立連接還是重連,Master都將啟動一個后臺進程,將內存數(shù)據(jù)以快照的方式寫入文件中,同時Master主進程開始收集新的命令。Master后臺進程完成快照操作以后,將數(shù)據(jù)文件發(fā)送給Slave,Slave將文件保存到磁盤上,然后把數(shù)據(jù)加載到內存中。接著Master把緩存的命令發(fā)給Slave,后續(xù)Master收到的寫命令都通過開始建立的連接發(fā)送到Slave。當Master與Slave斷開連接,Slave自動重新建立連接。當Master同時收到多個Slave發(fā)來的同步請求,只會啟動一個進程寫數(shù)據(jù)庫鏡像,然后發(fā)給所有的Slave。
主從同步第一階段(建立連接):
- Slave服務器主動連接到Master服務器。
- Slave服務器發(fā)送SYNC命令到Master服務器請求同步。
- Master服務器啟動新的進程備份數(shù)據(jù)庫到rdb文件。
- Master服務器把rdb文件傳輸給Slave服務器。
- Slave服務器清空數(shù)據(jù)庫文件,把rdb文件導入數(shù)據(jù)庫中。
主從同步第二階段(持續(xù)同步):
- Master服務器把所有用戶的更改數(shù)據(jù)的操作,通過命令的形式轉發(fā)給所有的Slave服務器。
- Slave服務器執(zhí)行Master服務器發(fā)送的命令。
Redis主從復制的配置,只需要在Slave服務器的配置文件中加入以下配置項:
slaveof 192.168.1.1 6379 # 指定Master的ip和端口
五、布隆過濾器
布隆過濾器是一種比較巧妙的概率型數(shù)據(jù)結構,它可以告訴你某種東西可能存在或者一定不存在。當它告訴你某種東西存在時,這種東西可能存在;當它告訴你某種東西不存在時,那它一定不存在。
1.1 實現(xiàn)原理
布隆過濾器的數(shù)據(jù)結構就是一個很大的位數(shù)組和幾個不同的的無偏哈希函數(shù)。向布隆過濾器添加元素時,會根據(jù)多個無偏哈希函數(shù),算出一個整體的索引值,然后對位數(shù)組長度進行一個取模運算,每個無偏哈希函數(shù)都會得到一個不同的位置。再把這幾個位數(shù)組對應的位置值置1,就完成了一個bf.add命令操作。
向布隆過濾器查詢元素是否存在時,和添加元素一樣,也會哈希出幾個位置來,看對應的位置是否都為1。只要有一個位0,那么就說明這個布隆過濾器不存在這個元素;如果都為1,并不能完全說明這個元素就一定存在,有可能這些位置為1是因為其他元素的存在,這就是布隆過濾器會存在誤判的原因。
1.2 基本用法
布隆過濾器的基本用法:
- bf.add:添加單個元素, 類似于集合的sadd。
- bf.madd:添加多個元素。
- bf.exists:判斷某個元素是否存在,類似于集合的sismember。
- bf.mexusts:判斷多個元素是否存在。
1.3 進階用法
創(chuàng)建一個自定義的布隆過濾器:bf.reserve key error_rate capacity
- key:鍵名。
- error_rate:期望錯誤率,錯誤率越低,需要的空間越大。
- capacity:初識容量,當初實際元素個數(shù)超過這個實際容量時,錯誤率將會上升。
bf.reserve one-more-filter 0.0001 1000000
六、Redis集群
redis集群主要是redis的一個分布式實現(xiàn),主要實現(xiàn)目標是:
- 可擴展性:能夠方便地擴展集群,可擴展到1000個節(jié)點。
- 寫入安全:集群會盡可能保存客戶端寫入的數(shù)據(jù),但在故障轉移時可能出現(xiàn)短時的數(shù)據(jù)丟失。
- 可用性:集群的大多數(shù)節(jié)點都是可達的,并且對于不可達的主節(jié)點都至少有一個從節(jié)點數(shù)可達的情況下,集群仍可以繼續(xù)提供服務。
redis集群采用的是p2p的運行模式,完全去中心化。把所有的key分成了16384個solt(插槽),每個redis實例負責其中一部分solt。集群中的所有信息都通過節(jié)點之間定期的數(shù)據(jù)交換而更新。
1. 集群搭建
redis集群至少需要6個節(jié)點(3主3從)才能完成,下面演示的是單臺機器上用多個端口進行模擬,生產環(huán)境中必須在多臺機器上搭建集群才能保證其可用性,以免因機器宕機而造成集群不可用。
bind 192.168.128.1 #綁定IP
port 9000 #指定端口
dir /usr/local/etc/redis/9000 #指定保存數(shù)據(jù)文件的路徑
cluster-enabled yes #開啟集群
cluster-config-file /usr/local/etc/redis/9000/nodes.conf #自動創(chuàng)建,保存集群運行情況
cluster-node-timeout 15000 #指定集群中節(jié)點的超時時間
cluster-require-full-coverage no #指定集群如果沒有完全汗覆蓋16384個槽時集群停止服務,默認yes,一定要設置為no,否則集群可能因為某個節(jié)點宕機而停止服務。
daemonize yes #指定是否以守護進程模式運行
logfile /usr/local/etc/redis/9000/redis.log #指定保存日志的文件路徑
2. 集群管理
redis提供了一個管理集群的腳本工具:redis-trib.rb,需要提前安裝ruby和rubygems。
3. 集群建立
執(zhí)行命令啟動節(jié)點:
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9000/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9001/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9002/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9003/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9004/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9005/redis.conf
執(zhí)行完畢后可以用ps -aux | grep redis查看節(jié)點是否啟動成功,啟動成功后使用redis-trib.rb的create命令進行集群的創(chuàng)建:
./redis-trib.rb create --replicas 1 192.168.128.1:9000 192.168.128.1:9001 192.168.128.1:9002 192.168.128.1:9003 192.168.128.1:9004 192.168.128.1:9005
其中“--replicas”選項的作用是指定每個主節(jié)點需要一個從節(jié)點。看到“[OK] All 16384 solts covered”就表示集群搭建成功了。
4. 添加節(jié)點
復制兩份配置文件,然后根據(jù)對應的節(jié)點進行修改,修改完成后執(zhí)行:
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9007/redis.conf
/usr/local/redis/bin/redis-server /usr/local/etc/redis/9008/redis.conf
啟動完畢后,使用redis-trib.rb的add-node命令可以把節(jié)點加入到集群中,一個作主節(jié)點,一個作從節(jié)點。
./redis-trib.rb add-node 192.168.128.1:9007 192.168.128.1:9008
上述命令以9007為主節(jié)點,9008為從節(jié)點添加到集群中,但要讓9008節(jié)點從屬于9007節(jié)點,我們必須添加9007節(jié)點的ID,在redis-cli中用cluster nodes命令可以查詢集群狀態(tài),得到類似"dd62920df907f18a290e4f6ff8d7f94832ccf993"這樣的ID,然后我們把上面的命令修改一下:
./redis-trib.rb add-node --slave --master -id dd62920df907f18a290e4f6ff8d7f94832ccf993 192.168.128.1:9007 192.168.128.1:9008
5. 數(shù)據(jù)遷移
上面的配置沒有為添加的節(jié)點分配任何的solt,所以新增的數(shù)據(jù)不會儲存在這個節(jié)點上。為了讓新增的節(jié)點分擔集群的儲存壓力,我們可以使用redis-trib.rb的reshard命令把集群的一部分solt遷移到新增的節(jié)點上:
./redis-trin.rb reshard 192.168.128.1:9000
回車之后需要我們輸入要遷移的solt個數(shù)和遷移的目標節(jié)點ID
6. 故障轉移
在redis集群中,某個主節(jié)點宕機不會導致整個集群停止服務,這是靠故障轉移來保證集群的高可用性。當某個主節(jié)點宕機,集群會從此節(jié)點的所有從節(jié)點選舉出一個節(jié)點你作為新的主節(jié)點。
六、代碼演示(PHP)
<?php
/**
* Redis 操作,支持 Master/Slave 的負載集群
*/
class RedisCluster
{
// 是否使用 M/S 的讀寫集群方案
private $_isUseCluster = false;
// Slave 句柄標記
private $_sn = 0;
// 服務器連接句柄
private $_linkHandle = array(
'master' => null, // 只支持一臺 Master
'slave' => array(), // 可以有多臺 Slave
);
/**
* 構造函數(shù)
*
* @param boolean $isUseCluster 是否采用 M/S 方案
*/
public function __construct($isUseCluster = false)
{
$this->_isUseCluster = $isUseCluster;
}
/**
* 連接服務器,注意:這里使用長連接,提高效率,但不會自動關閉
*
* @param array $config Redis服務器配置
* @param boolean $isMaster 當前添加的服務器是否為 Master 服務器
* @return boolean
*/
public function connect($config = array('host' => '127.0.0.1', 'port' => 6379), $isMaster = true)
{
// default port
if (!isset($config['port'])) {
$config['port'] = 6379;
}
// 設置 Master 連接
if ($isMaster) {
$this->_linkHandle['master'] = new \Redis();
$ret = $this->_linkHandle['master']->pconnect($config['host'], $config['port']);
} else {
// 多個 Slave 連接
$this->_linkHandle['slave'][$this->_sn] = new Redis();
$ret = $this->_linkHandle['slave'][$this->_sn]->pconnect($config['host'], $config['port']);
++$this->_sn;
}
return $ret;
}
/**
* 關閉連接
*
* @param int $flag 關閉選擇 0:關閉 Master 1:關閉 Slave 2:關閉所有
* @return boolean
*/
public function close($flag = 2)
{
switch ($flag) {
// 關閉 Master
case 0:
$this->getRedis()->close();
break;
// 關閉 Slave
case 1:
for ($i = 0; $i < $this->_sn; ++$i) {
$this->_linkHandle['slave'][$i]->close();
}
break;
// 關閉所有
case 1:
$this->getRedis()->close();
for ($i = 0; $i < $this->_sn; ++$i) {
$this->_linkHandle['slave'][$i]->close();
}
break;
}
return true;
}
/**
* 得到 Redis 原始對象可以有更多的操作
*
* @param boolean $isMaster 返回服務器的類型 true:返回Master false:返回Slave
* @param boolean $slaveOne 返回的Slave選擇 true:負載均衡隨機返回一個Slave選擇 false:返回所有的Slave選擇
* @return redis object
*/
public function getRedis($isMaster = true, $slaveOne = true)
{
// 只返回 Master
if ($isMaster) {
return $this->_linkHandle['master'];
} else {
return $slaveOne ? $this->_getSlaveRedis() : $this->_linkHandle['slave'];
}
}
/**
* 寫緩存
*
* @param string $key 組存KEY
* @param string $value 緩存值
* @param int $expire 過期時間, 0:表示無過期時間
*/
public function set($key, $value, $expire = 0)
{
// 永不超時
if ($expire == 0) {
$ret = $this->getRedis()->set($key, $value);
} else {
$ret = $this->getRedis()->setex($key, $expire, $value);
}
return $ret;
}
/**
* 讀緩存
*
* @param string $key 緩存KEY,支持一次取多個 $key = array('key1','key2')
* @return string || boolean 失敗返回 false, 成功返回字符串
*/
public function get($key)
{
// 是否一次取多個值
$func = is_array($key) ? 'mGet' : 'get';
// 沒有使用M/S
if (!$this->_isUseCluster) {
return $this->getRedis()->{$func}($key);
}
// 使用了 M/S
return $this->_getSlaveRedis()->{$func}($key);
}
/*
// magic function
public function __call($name,$arguments){
return call_user_func($name,$arguments);
}
*/
/**
* 條件形式設置緩存,如果 key 不存時就設置,存在時設置失敗
*
* @param string $key 緩存KEY
* @param string $value 緩存值
* @return boolean
*/
public function setnx($key, $value)
{
return $this->getRedis()->setnx($key, $value);
}
/**
* 刪除緩存
*
* @param string || array $key 緩存KEY,支持單個健:"key1" 或多個健:array('key1','key2')
* @return int 刪除的健的數(shù)量
*/
public function remove($key)
{
// $key => "key1" || array('key1','key2')
return $this->getRedis()->delete($key);
}
/**
* 值加加操作,類似 ++$i ,如果 key 不存在時自動設置為 0 后進行加加操作
*
* @param string $key 緩存KEY
* @param int $default 操作時的默認值
* @return int 操作后的值
*/
public function incr($key, $default = 1)
{
if ($default == 1) {
return $this->getRedis()->incr($key);
} else {
return $this->getRedis()->incrBy($key, $default);
}
}
/**
* 值減減操作,類似 --$i ,如果 key 不存在時自動設置為 0 后進行減減操作
*
* @param string $key 緩存KEY
* @param int $default 操作時的默認值
* @return int 操作后的值
*/
public function decr($key, $default = 1)
{
if ($default == 1) {
return $this->getRedis()->decr($key);
} else {
return $this->getRedis()->decrBy($key, $default);
}
}
/**
* 添空當前數(shù)據(jù)庫
*
* @return boolean
*/
public function clear()
{
return $this->getRedis()->flushDB();
}
/* =================== 以下私有方法 =================== */
/**
* 隨機 HASH 得到 Redis Slave 服務器句柄
*
* @return redis object
*/
private function _getSlaveRedis()
{
// 就一臺 Slave 機直接返回
if ($this->_sn <= 1) {
return $this->_linkHandle['slave'][0];
}
// 隨機 Hash 得到 Slave 的句柄
$hash = $this->_hashId(mt_rand(), $this->_sn);
return $this->_linkHandle['slave'][$hash];
}
/**
* 根據(jù)ID得到 hash 后 0~m-1 之間的值
*
* @param string $id
* @param int $m
* @return int
*/
private function _hashId($id, $m = 10)
{
//把字符串K轉換為 0~m-1 之間的一個值作為對應記錄的散列地址
$k = md5($id);
$l = strlen($k);
$b = bin2hex($k);
$h = 0;
for ($i = 0; $i < $l; $i++) {
//相加模式HASH
$h += substr($b, $i * 2, 2);
}
$hash = ($h * 1) % $m;
return $hash;
}
/**
* lpush
*/
public function lpush($key, $value)
{
return $this->getRedis()->lpush($key, $value);
}
/**
* rpush
*/
public function rpush($key, $value)
{
return $this->getRedis()->rpush($key, $value);
}
/**
* add lpop
*/
public function lpop($key)
{
return $this->getRedis()->lpop($key);
}
/**
* add rpop
*/
public function rpop($key)
{
return $this->getRedis()->rpop($key);
}
/**
* lrange
*/
public function lrange($key, $start, $end)
{
return $this->getRedis()->lrange($key, $start, $end);
}
/**
* rrange
*/
public function rrange($key, $start, $end)
{
return $this->getRedis()->rrange($key, $start, $end);
}
/**
* set hash opeation
*/
public function hset($name, $key, $value)
{
if (is_array($value)) {
return $this->getRedis()->hset($name, $key, serialize($value));
}
return $this->getRedis()->hset($name, $key, $value);
}
/**
* get hash opeation
*/
public function hget($name, $key = null, $serialize = true)
{
if ($key) {
$row = $this->getRedis()->hget($name, $key);
if ($row && $serialize) {
unserialize($row);
}
return $row;
}
return $this->getRedis()->hgetAll($name);
}
/**
* delete hash opeation
*/
public function hdel($name, $key = null)
{
if ($key) {
return $this->getRedis()->hdel($name, $key);
}
return $this->getRedis()->hdel($name);
}
/**
* Transaction start
*/
public function multi()
{
return $this->getRedis()->multi();
}
/**
* Transaction send
*/
public function exec()
{
return $this->getRedis()->exec();
}
/** 集合操作 **/
/*
* 將一個元素加入集合,已經存在集合中的元素則忽略。
* 若集合不存在則先創(chuàng)建,若key不是集合類型則返回false,若元素已存在返回0,插入成功返回1。
*/
public function sAdd($key, $value)
{
return $this->getRedis()->sAdd($key, $value);
}
/*
* 返回集合中所有成員。
*/
public function sMembers($key)
{
return $this->getRedis()->sMembers($key);
}
/*
* 判斷集合里是否存在指定元素,是返回true,否則返回false。
*/
public function sismember($key, $value)
{
return $this->getRedis()->sismember($key, $value);
}
/*
* 返回集合中元素的數(shù)量
*/
public function scard($key)
{
return $this->getRedis()->scard($key);
}
/*
* 隨機刪除并返回集合里的一個元素。
*/
public function sPop($key)
{
return $this->getRedis()->sPop($key);
}
/*
* 隨機返回(n)個集合內的元素,由第二個參數(shù)決定返回多少個
* 如果 n 大于集合內元素的個數(shù)則返回整個集合
* 如果 n 是負數(shù)時隨機返回 n 的絕對值,數(shù)組內的元素會重復出現(xiàn)
*/
public function sRandMember($key, $n)
{
return $this->getRedis()->sRandMember($key, $n);
}
/*
* 刪除集合中指定的一個元素,元素不存在返回0。刪除成功返回1,否則返回0。
*/
public function srem($key, $value)
{
return $this->getRedis()->srem($key, $value);
}
/*
* 模糊搜索相對的元素,
* 參數(shù):key,迭代器變量,匹配值,每次返回元素數(shù)量(默認為10個)
*/
public function sscan($key, $it, $n = 10)
{
// return $this->getRedis()->sscan($key, $it, 's*', $n);
}
}