Redis做爲當下最流行的內存Nosql數據庫,有着諸多的應用場景。在不一樣的應用場景,對Redis的部署、配置以及使用方式都存在的不一樣地方。根據個人工做經驗,把隊列、緩存、歸併、去重等應用場景的「最佳實踐」整理以下。php
本文中的全部代碼,都可在github上找到:https://github.com/huyanping/RedisStudyhtml
隊列
Redis的list數據結構常常會被用做隊列來使用,經常使用的方法有:lpop/rpop、lpush/rpush、llen、lindex等。因爲Redis提供的list是一個雙向鏈表,咱們也能夠把list當作棧來使用。使用Redis的list做爲隊列時,須要注意如下幾個問題:git
- 隊列中的數據通常具備比較高的可靠性要求,Redis的持久化機制最好使用AOF方式,保證數據不丟失
- 一樣因爲對數據的可靠性要求較高,內存監控尤其重要,若是出現隊列堆積內存用光形成沒法提供服務的狀況
- 若是隻有一個消費者在消費隊列,推薦使用lindex先讀取消息,消費完以後在lpop扔掉,這樣能夠保證事務性,避免消息處理失敗後消息丟失
- 若是是多個消費者在消費隊列,消息處理失敗的狀況下能夠將消息從新寫入隊列,前期是消息沒有有序性要求
經過批量(multi)和並行的方式能夠提升生產者和消費者的處理能力。批量處理能夠減小網絡通訊量,同時減小Redis在不一樣任務間切換的開銷。並行的好處就是當一個客戶端在準備或處理數據時而且Redis空閒時,另外一個客戶端能夠從Redis讀取數據;這樣能夠儘可能保證Redis始終保持在繁忙狀態。github
若是經過以上優化,仍然有隊列堆積的狀況,建議啓動多個Redis實例。因爲Redis是單線程模型,沒法利用多核CPU,開啓多個實例可以明顯提高吐吞量。Redis的集羣方案有不少種,也能夠簡單的在客戶端使用hash算法實現。具體實現方案已經超出本文敘述範圍,再也不累贅。redis
基於Redis list的消息隊列使用示例代碼以下:算法
1sql 2數據庫 3緩存 4服務器 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php // 生產者 namespace jenner\redis\study\queue; use Jenner\SimpleFork\Process; use Jenner\SimpleFork\Queue\RedisQueue; class Producer extends Process { /** * start producer process */ public function run() { $queue = new RedisQueue( '127.0.0.1' , 6379, 1); for ( $i = 0; $i < 100000; $i ++) { $queue ->put( getmypid () . '-' . mt_rand(0, 1000)); } $queue ->close(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php // 消費者 namespace jenner\redis\study\queue; use Jenner\SimpleFork\Process; use Jenner\SimpleFork\Queue\RedisQueue; class Consumer extends Process { /** * start consumer process */ public function run() { $queue = new RedisQueue( '127.0.0.1' , 6379, 1); while (true) { $res = $queue ->get(); if ( $res !== false) { echo $res . PHP_EOL; } else { break ; } } } } |
緩存
這裏咱們說的緩存,是能夠丟失或過時的數據,不能丟失或過時的緩存(或者應該叫作數據庫了)不在本文的敘述範圍。Redis的字典結構經常使用來作緩存使用,經常使用的方法有:set/get、hget/hgetall/hset等。使用redis做爲緩存時,須要注意如下幾個問題:
- 因爲Redis可用的內存是有限的,不能容忍redis內存的無限增長,最好設置最大內存maxmemory
- 在開啓maxmemory的狀況下,能夠啓用lru機制,設置key的expire,當到達Redis最大內存時,Redis會根據最近最少用算法對key進行自動淘汰;lru的策略有6種,可參考:http://www.aikaiyuan.com/7089.html
- Redis的持久化策略和Redis故障恢復時間是一個博弈的過程,若是你但願在發生故障時可以儘快恢復,應該啓用dump備份機制,但dump機制要求你必須保留至少1/3(經驗值)的可用內存(寫時複製),因此你可能沒辦法分配儘量多的內存給Redis;若是可以容忍Redis漫長的故障恢復時間,可使用AOF持久化機制,同時關閉dump機制,這樣能夠突破保留1/3內存的限制。
關於緩存的使用方法,不屬於本文敘述範圍,可參考:http://tech.meituan.com/avalanche-study.html
示例代碼太多,這裏就只貼個地址:https://github.com/huyanping/RedisStudy/tree/master/src/spider
計算
Redis提供的原子遞增遞減方法以及有序集合等能夠承擔一些計算任務,例如訪問量統計等。經常使用的方法有:incr/decr、hincrby、zadd/zcard等。
在使用redis做爲計算服務時,須要注意一下幾個問題:
- 計算場景的數據通常對可靠性要求比較高,建議啓用AOF持久化機制,根據恢復時間和內容利用率的考慮肯定是否開啓dump機制。
- redis的單線程模型決定了redis沒法利用多核CPU,這裏建議引入redis集羣解決方案,固然仍然能夠在客戶端經過hash方案解決。
- 批量發送、批量導出
去重
Redis的hset和HyperLogLog數據結構能夠在使用少許內存的狀況下對數據進行去重。在有大量數據須要去重的場景比較試用。Redis的HyperLogLog只須要使用12K的內存空間便可對2的64次方個記錄進行去重。具體選用哈希字典仍是HyperLogLog須要根據你須要去重的數據量綜合決定,若是你須要去重的數據整體佔用空間遠小於12K,使用哈希字典便可,若是超過12K,推薦使用HyperLogLog。經常使用的命令有:pfadd/pfcount、hset/hlen。這裏須要注意,pfadd返回的是布爾型,表示該值是否已經存在;不能夠經過累加pfadd的結果斷定惟一記錄數,必須調用pfcount獲取,這個應該是算法的緣由,記住就好。有興趣的童鞋能夠深究一下HyperLogLog的算法。
兩種方式的示例代碼分別以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?php // HyperLogLog namespace jenner\redis\study\unique; use jenner\redis\study\tool\Logger; class HyperLogLog { /** * @var \Redis */ protected $redis ; /** * @var array */ protected $ips ; /** * default hyperloglog key */ const KEY = "ip-unique-hyperloglog" ; /** * HyperLogLog constructor. * @param array $ips */ public function __construct( array $ips ) { $this ->redis = new \Redis(); $this ->redis->connect( "127.0.0.1" , 6379); $this ->redis->select(3); $this ->ips = $ips ; } /** * start to count ips using hyperloglog */ public function start() { Logger::info( "unique process start" ); $this ->redis->pfadd(self::KEY, $this ->ips); Logger::info( "unique done. ip count:" . $this ->redis->pfcount(self::KEY)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php // set namespace jenner\redis\study\unique; use jenner\redis\study\tool\Logger; class Set { /** * @var \Redis */ protected $redis ; /** * @var array */ protected $ips ; /** * */ const KEY = "ip-unique-normal" ; /** * Set constructor. * @param array $ips */ public function __construct( array $ips ) { $this ->redis = new \Redis(); $this ->redis->connect( "127.0.0.1" , 6379); $this ->redis->select(3); $this ->ips = $ips ; } /** * start to count ips using set */ public function start() { Logger::info( "unique process start" ); foreach ( $this ->ips as $ip ) { $this ->redis->sAdd(self::KEY, $ip ); } Logger::info( "unique done. ip count:" . $this ->redis->sCard(self::KEY)); } } |
發佈訂閱
Redis的發佈訂閱機制,在客戶端與服務端因爲某些問題連接失效時,中間訂閱的數據會丟失;因此在實際生產環境中,不多應用這種機制。
示例代碼以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php // publisher namespace jenner\redis\study\pubsub; class Publisher { /** * @var \Redis */ protected $redis ; /** * default pubsub key */ const KEY = "pubsub-demo" ; /** * Publisher constructor. */ public function __construct() { $this ->redis = new \Redis(); $this ->redis->connect( "127.0.0.1" , 6379); $this ->redis->select(4); } public function publish() { $count = 10; for ( $i = 0; $i < $count ; $i ++) { $this ->redis->publish(self::KEY, mt_rand(0, 10000)); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php // subscriber namespace jenner\redis\study\pubsub; use jenner\redis\study\tool\Logger; class Subscriber { /** * default pubsub key */ const KEY = "pubsub-demo" ; /** * @var \Redis */ protected $redis ; /** * Subscriber constructor. */ public function __construct() { $this ->redis = new \Redis(); $this ->redis->connect( "127.0.0.1" , 6379); $this ->redis->select(4); } public function subscribe() { $this ->redis->subscribe( array (self::KEY), function ( $redis , $channel , $message ) { Logger::info( "get message[" . $message . "] from channel[" . $channel . "]" ); }); } } |
Redis lua應用
Lua 腳本功能是 Reids 2.6 版本的最大亮點, 經過內嵌對 Lua 環境的支持, Redis 解決了長久以來不能高效地處理 CAS (check-and-set)命令的缺點, 而且能夠經過組合使用多個命令, 輕鬆實現之前很難實現或者不能高效實現的模式。
優勢:原子性(因爲Redis是單線程模型,同一時刻只能處理一個lua腳本),更小的請求包
應用場景:事務實現,批量處理
如下示例代碼實現了getAndSet命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?php // redis lua namespace jenner\redis\study\lua; use jenner\redis\study\tool\Logger; class Lua { /** * @var \Redis */ protected $redis ; /** * Lua constructor. */ public function __construct() { $this ->redis = new \Redis(); $this ->redis->connect( "127.0.0.1" , 6379); $this ->redis->select(2); } /** * @param $key * @param $value * @return bool */ public function set( $key , $value ) { return $this ->redis->set( $key , $value ); } /** * @param $key * @param $value */ public function getAndSet( $key , $value ) { $lua = <<<GLOB_MARK local value = redis.call( 'get' , KEYS[1]) redis.call( 'set' , KEYS[1], ARGV[1]) return value GLOB_MARK; $result = $this ->redis-> eval ( $lua , array ( $key , $value ), 1); Logger::info( "eval script result:" . var_export( $result , true)); } /** * @return string */ public function error() { return $this ->redis->getLastError(); } } |
Dump故障
當Redis使用內存大於操做系統剩餘內存的2倍時,使用dump持久化機制可能會形成服務器宕機、假死等狀況。緣由是dump時,Redis會fork一個子進程,根據寫實複製原則,若是Redis中的數據會發生修改時,操做系統會把服務進程的內存copy一份給子進程,具體copy多少根據數據修改的覆蓋度;這時若是內存不夠用,操做系統會使用swap擴展內存,性能急劇降低,若是swap也不夠了,則可能發生宕機、假死等狀況。
解決方案:設置maxmemory,監控Redis內存使用(Redis info命令),場景容許的狀況下開啓lru機制。
maxmemory故障
故障描述:設置了maxmemory,內存用完,客戶端沒法寫入
解決方案:對Redis內存使用進行監控,根據業務場景控制內存使用;若是內存確實不夠用了,考慮引入分佈式Redis集羣方案
redis訪問漏洞
這個漏洞的原理很是簡單,只需執行以幾條命令便可:
1 2 3 4 |
redis> config set dbfilename authorized_keys redis> config set dir '/root/.ssh' redis> set xxoo "\n\n\nyour public ssh key" redis> save |
經過以上幾條命令,能夠將你的ssh公鑰寫入對方的Redis服務器,從而獲取root權限。這個漏洞的利用條件也比較苛刻,須要知足如下幾個條件:
- Redis需是root用戶運行,或已知Redis運行用戶
- 6379端口無防火牆攔截
- Redis無訪問密碼
- config set命令沒有被禁用
根據以上利用條件,對應防護手段以下:
- 優先監聽127.0.0.1網卡(如過redis是給本機訪問),優先監聽內網網卡
- 防火牆對6379端口訪問進行限制
- 使用非root用戶運行redis
- redis開啓密碼訪問(養成好習慣)
- 禁用config set命令
通常狀況下作到第二點,基本就不會被黑了,但咱們應該儘可能作到第四點,盡善盡美。
redis自動重連
RedisRetry是一個支持自動重連的redis客戶端封裝,項目地址:https://github.com/huyanping/RedisRetry
原理:使用__call方法對redis的原生方法封裝,當發生RedisException時,自動關閉並從新創建鏈接,執行n次,每次相隔m毫秒。
常量:
REDIS_RETRY_TIMES 重試次數
REDIS_RETRY_DELAY 間隔時間,單位毫秒
使用方式:把使用Redis類的地方,添加’use \Jenner\RedisRetry\Redis’便可。