初識Redis

目錄

各命令及底層實現

遠程 redis 服務上執行命令:

字符串(String)

哈希(Hash)

列表(List)

集合(Set)

有序集合

發佈訂閱

redis和memcache的區別

穿透、緩存雪崩

使用redis實現分佈式鎖

併發競爭問題

持久化的幾種方式、優缺點、怎麼實現

緩存失敗策略

redis集羣高可用原理

redis緩存分片(分區)

redis的數據淘汰策略


各命令及底層實現

遠程 redis 服務上執行命令:

redis-cli -h host -p port -a password

字符串(String)

字符串數據類型的相關命令用於管理 redis 字符串值

SET key value

設置指定 key 的值

GET key

獲取指定 key 的值

GETRANGE kay start end

返回 key 中字符串值的子字符

GETSET key value

將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。

哈希(Hash)

Redis hash 是一個string類型的field和value的映射表,hash特別適合用於存儲對象。

Redis 中每個 hash 可以存儲 232 - 1 鍵值對(40多億)。     

HSET key field value

將哈希表 key 中的字段 field 的值設爲 value 。

HGET key field

獲取存儲在哈希表中指定字段的值。

HDELETE key field1[field2]

刪除一個或多個哈希表字段

列表(List)

Redis列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)

一個列表最多可以包含 232 - 1 個元素 (4294967295, 每個列表超過40億個元素)。

LPUSH key value1[value2]

將一個或多個值插入到列表頭部

LPUSHX key value

將一個值插入到已存在的列表頭部

LPOP key

移出並獲取列表的第一個元素

LLEN key

獲取列表長度

 

集合(Set)

Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味着集合中不能出現重複的數據。

Redis 中集合是通過哈希表實現的,所以添加,刪除,查找的複雜度都是 O(1)。

集合中最大的成員數爲 232 - 1 (4294967295, 每個集合可存儲40多億個成員)。

SADD key member1 [member2]

向集合添加一個或多個成員

SMEMBERS key

返回集合中的所有成員

SISMEMBER key member

判斷 member 元素是否是集合 key 的成員

SPOP key

移除並返回集合中的一個隨機元素

有序集合

Redis 有序集合和集合一樣也是string類型元素的集合,且不允許重複的成員。

不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來爲集合中的成員進行從小到大的排序。

有序集合的成員是唯一的,但分數(score)卻可以重複。

集合是通過哈希表實現的,所以添加,刪除,查找的複雜度都是O(1)。 集合中最大的成員數爲 232 - 1 (4294967295, 每個集合可存儲40多億個成員)。

ZADD key score1 member1 [score2 member2]

向有序集合添加一個或多個成員,或者更新已存在成員的分數

 

ZCARD key

獲取有序集合的成員數

ZCOUNT key min max

計算在有序集合中指定區間分數的成員數

發佈訂閱

Redis 發佈訂閱(pub/sub)是一種消息通信模式:發送者(pub)發送消息,訂閱者(sub)接收消息。

Redis 客戶端可以訂閱任意數量的頻道。

頻道Channel1,以及訂閱這三個頻道的客戶端—— client2 、 client5 和 client1 之間的關係:

當有新消息通過 PUBLISH 命令發送給頻道 channel1 時, 這個消息就會被髮送給訂閱它的三個客戶端:

SUBSCRIBE channel [channel ...]

訂閱給定的一個或多個頻道的信息。

PSUBSCRIBE pattern [pattern ...]

訂閱一個或多個符合給定模式的頻道。

UNSUBSCRIBE [channel [channel ...]]

退訂給定的頻道。

PUNSUBSCRIBE [pattern [pattern ...]]

退訂所有給定模式的頻道。

PUBLISH channel message

將信息發送到指定的頻道。

PUBSUB subcommand [argument [argument ...]]

查看訂閱與發佈系統狀態。

redis和memcache的區別

memcache是分佈式高速緩存系統。

共同點:都是內存數據庫

區別:

memcache:

  1. 可以利用多核優勢,單實例吞吐量極高,可以達到幾十萬QPS,適用於大數據量
  2. 只支持簡單的key/value數據結構,redis支持豐富的數據類型
  3. 無法進行持久化,數據不能備份,只能用於緩存,重啓後數據全部丟失

redis:

  1. 支持多種數據結構,如string、list、set、zset、dict等
  2. 單線程請求,所有命令串行執行,併發情況下不需要考慮數據一致性
  3. 支持持久化,可以是使用AOF及RDB數據持久化到磁盤,從而進行數據的備份或數據恢復等操作,防止數據的丟失。
  4. 支持通過Replication進行數據複製,通過master-slave機制,可以實現實時進行數據的同步複製,支持多級複製和增量複製。
  5. 支持pub/sub消息訂閱機制,可以用來進行消息訂閱與通知
  6. 支持簡單的事務,使用場景很少,還不成熟

穿透、緩存雪崩

穿透

描述:

   指在redis中查詢一個一定不存在的數據時,需要從數據庫中查詢。查詢到數據不存在時則不寫入緩存,這將導致每次在redis中獲取不存在的數據時都會去數據庫中查詢,這將導致緩存穿透。

解決方法:

  1. 使用布隆過濾,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據就會被攔截,這樣就可以避免對數據庫的查詢壓力。
  2. 如果在數據庫查詢的數據爲空,不管何種原因造成的,仍然將空結果進行緩存,只是設置的過期時間很短,最長不超過5分鐘。

緩存雪崩

描述:

   Redis的緩存集中在一段時間內失效,發生大量的穿透,將所有的查詢落到數據庫中,造成了緩存雪崩。

解決方法:

   1、緩存失效後,通過加鎖或隊列來控制數據庫寫緩存的線程數量。如一個key只允許一個線程查詢數據和寫緩存,其他線程等待。

2、可以通過緩存reload機制,預先去更新緩存,再即將發生大併發訪問前手動觸發加載緩存。

3、不同的key設置不同的過期失效時間,讓緩存失效時間點儘量均勻。

4、做好二級緩存或者雙緩存策略。A1爲原始緩存,A2爲拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置爲短期,A2設置爲長期。

 

使用redis實現分佈式鎖

分佈式鎖一般有三種實現方式:1. 數據庫樂觀鎖;2. 基於Redis的分佈式鎖;3. 基於ZooKeeper的分佈式鎖。

1、SETNX是set if not exist的縮寫,如果不存在就返回保存value並返回1,如果存在就返回0。將key設置值爲value,如果key不存在,這種情況下等同SET命令。 當key存在時,什麼也不做。

2、GETSET其實就是兩個指令GET和SET,首先會GET到當前key的值並返回,然後在設置當前Key爲要設置Value。

public class RedisLock {
   
private static final String LOCK_SUCCESS = "OK";
   
private static final String SET_IF_NOT_EXIST = "NX";
   
private static final String SET_WITH_EXPIRE_TIME = "PX";
   
private final JedisPool jedisPool;

   
public RedisLock(JedisPool jedisPool) {
       
this.jedisPool = jedisPool;
    }

   
/**
     *
嘗試獲取分佈式鎖
     * @param
jedis Redis客戶端
     * @param
lockKey
     * @param
requestId 請求標識
     * @param
expireTime 超期時間
     * @return 是否獲取成功
     */
   
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId,
SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

       
if (LOCK_SUCCESS.equals(result)) {
           
return true;
        }
       
return false;

    }

   
/**
     *
加鎖
     * @param
locaName  鎖的key
     * @param
acquireTimeout  獲取超時時間
     * @param
timeout   鎖的超時時間
     * @return 鎖標識
     */
   
public String lockWithTimeout(String locaName,
                                 
long acquireTimeout, long timeout) {
        Jedis conn =
null;
        String retIdentifier =
null;
       
try {
           
// 獲取連接
           
conn = jedisPool.getResource();
           
// 隨機生成一個value
           
String identifier = UUID.randomUUID().toString();
           
// 鎖名,即key值
           
String lockKey = "lock:" + locaName;
           
// 超時時間,上鎖後超過此時間則自動釋放鎖
           
int lockExpire = (int)(timeout / 1000);

           
// 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
           
long end = System.currentTimeMillis() + acquireTimeout;
           
while (System.currentTimeMillis() < end) {
               
if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                   
// 返回value值,用於釋放鎖時間確認
                   
retIdentifier = identifier;
                   
return retIdentifier;
                }
                
// 返回-1代表key沒有設置超時時間,爲key設置一個超時時間
               
if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }

               
try {
                    Thread.sleep(
10);
                }
catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
catch (JedisException e) {
            e.printStackTrace();
        }
finally {
           
if (conn != null) {
                conn.close();
            }
        }
       
return retIdentifier;
    }

   
/**
     *
釋放鎖
     * @param
lockName 鎖的key
     * @param
identifier    釋放鎖的標識
     * @return
    
*/
   
public boolean releaseLock(String lockName, String identifier) {
        Jedis conn =
null;
        String lockKey =
"lock:" + lockName;
       
boolean retFlag = false;
       
try {
            conn =
jedisPool.getResource();
           
while (true) {
               
// 監視lock,準備開始事務
               
conn.watch(lockKey);
               
// 通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
               
if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                   
if (results == null) {
                        
continue;
                    }
                    retFlag =
true;
                }
                conn.unwatch();
               
break;
            }
        }
catch (JedisException e) {
            e.printStackTrace();
        }
finally {
           
if (conn != null) {
                conn.close();
            }
        }
       
return retFlag;
    }
}

 

併發競爭問題

Redis的併發競爭問題,主要是發生在併發寫競爭。

  1. 可以使用獨佔鎖的方式,類似操作系統的mutex機制。
  2. 使用樂觀鎖的方式進行解決(成本較低,非阻塞,性能較高)。

watch stock

get stock $stock

$stock = $stock - 10

multi

set stock $stock

exec

watch這裏表示監控該key值,後面的事務是有條件的執行,如果從watch的exec語句執行時,watch的key對應的value值被修改了,則事務不會執行。

WATCH key [key ...]

監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷。

UNWATCH

取消 WATCH 命令對所有 key 的監視。

MULTI

標記一個事務塊的開始。

EXEC

執行所有事務塊內的命令。

DISCARD

取消事務,放棄執行事務塊內的所有命令。

 

持久化的幾種方式、優缺點、怎麼實現

  1. RDB持久化:將Reids在內存中的數據庫記錄定時dump到磁盤上的RDB持久化。

配置:

通過配置文件來修改Redis服務器dump快照的頻率,在打開6379.conf文件之後,搜索save

save 900 1  #在900秒(15分鐘)之後,如果至少有1個key發生變化,則dump內存快照。

save 300 10 #在300秒(5分鐘)之後,如果至少有10個key發生變化,則dump內存快照。

save 60 10000 #在60秒(1分鐘)之後,如果至少有10000個key發生變化,則dump內存快照。

 

  1. AOF(append only file)持久化:原理是將Reids的操作日誌以追加的方式寫入文件。

配置:

在Redis的配置文件中存在三種同步方式,它們分別是:

appendfsync always  #每次有數據修改發生時都會寫入AOF文件。

appendfsync everysec  #每秒鐘同步一次,該策略爲AOF的缺省策略。

appendfsync no     #從不同步。高效但是數據不會被持久化。

 

緩存失敗策略

穿透、緩存雪崩

redis集羣高可用原理

兩種方式:

  1. 主備方式

一臺主機、一臺或多臺備機,在正常情況下主機對外提供服務,並把數據同步到備機,當主機宕機後,備機立刻開始服務。

優點:對客戶端無影響

缺點:絕大數情況下備機沒有使用。一直空閒浪費。

  1. 主從方式

主從之間進行數據同步。 當Master宕機後,通過選舉算法(Paxos、Raft)從slave中選舉出新Master繼續對外提供服務,主機恢復後以slave的身份重新加入。

優點:另一個目的是進行讀寫分離,這是當單機讀寫壓力過高的一種通用型解決方案。 其主機的角色只提供寫操作或少量的讀,把多餘讀請求通過負載均衡算法分流到單個或多個slave服務器上。

缺點:主機宕機後,Slave雖然被選舉成新Master了,但對外提供的IP服務地址卻發生變化了,意味着會影響到客戶端。 解決這種情況需要一些額外的工作,在當主機地址發生變化後及時通知到客戶端,客戶端收到新地址後,使用新地址繼續發送新請求

數據同步方式:

1、異步

2、同步

redis緩存分片(分區)

分區是分割數據到多個Redis實例的處理過程,因此每個實例只保存key的一個子集。

優點

缺點

  1. 利用多臺計算機內存的和值,允許我們構造更大的數據庫。
  2. 通過多核和多臺計算機,允許我們擴展計算能力;通過多臺計算機和網絡適配器,允許我們擴展網絡帶寬。
  1. 涉及多個key的操作通常是不被支持的。舉例來說,當兩個set映射到不同的redis實例上時,你就不能對這兩個set執行交集操作。
  2. 涉及多個key的redis事務不能使用。
  3. 當使用分區時,數據處理較爲複雜,比如你需要處理多個rdb/aof文件,並且從多個實例和主機備份持久化文件。
  4. 增加或刪除容量也比較複雜。redis集羣大多數支持在運行時增加、刪除節點的透明數據平衡的能力,但是類似於客戶端分區、代理等其他系統則不支持這項特性。然而,一種叫做presharding的技術對此是有幫助的。

分區類型:

  1. 範圍分區

最簡單的分區方式是按範圍分區,映射一定範圍的對象到特定的Redis實例

比如,ID從0到10000的用戶會保存到實例R0,ID從10001到 20000的用戶會保存到R1,以此類推。

這種方式是可行的,並且在實際中使用,不足就是要有一個區間範圍到實例的映射表。這個表要被管理,同時還需要各 種對象的映射表,通常對Redis來說並非是好的方法。

  1. 哈希分區

用一個hash函數將key轉換爲一個數字,比如使用crc32 hash函數。對key foobar執行crc32(foobar)會輸出類似93024922的整數。

對這個整數取模,將其轉化爲0-3之間的數字,就可以將這個整數映射到4個Redis實例中的一個了。93024922 % 4 = 2,就是說key foobar應該被存到R2實例中。注意:取模操作是取除的餘數,通常在多種編程語言中用%操作符實現。

redis的數據淘汰策略

redis內存的數據集大小到一定大小時,會實施數據淘汰策略。Redis有6種數據淘汰策略:

  1. volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰。
  2. volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
  3. volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰。
  4. allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰。
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰。
  6. no-enviction(驅逐):禁止驅逐數據。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章