此再見非彼再見,在之前的一篇文章中,學習了 Redis 的安裝和基本的使用,在邊實習邊學習的過程中,加深了對其的理解,所以在這裏總結一哈。
文章目錄
回顧 Redis
Redis 支持五種的數據類型:
String(字符串)、Hash(哈希)、List(列表)、Set(集合)及 ZSet(sorted set,有序集合)。
鍵 Key 的管理
命令
命令 | 作用 |
---|---|
DEL key | key 存在時刪除 key |
DUMP key | 序列化給定 key 並返回序列化的值 |
EXISTS key | 檢查 key 是否存在 |
EXPIRE key seconds | 爲給定 key 設置過期時間 |
PEXPIRE key milliseconds | 同上 |
TTL key | 以秒爲單位返回給定 key 的剩餘生存時間 |
PTTL key | 以毫秒爲單位,同上 |
PERSIST key | 移除 key 的過期時間,key 將持久保持 |
KEYS pattern | 查找所有符合給定模式的 key (* ?) |
RENAME key newkey | 修改 key 的名稱 |
MOVE key db | 將當前數據庫的 key 移動到給定的數據庫 db 中 |
TYPE key | 返回 key 所存儲的值的類型 |
最常用的就是 EXPIRE key seconds,應用場景:
- 限時優惠活動信息
- 需要定時更新的數據,積分榜
- 手機驗證碼
key 的命名建議:
- 不要太長,redis 單個 key 存入 512M 大小,且會降低查找效率
- 使用統一的命名模式提高 key 的可讀性,如 studen,userId
五種數據類型
String
最基本的 Redis 存儲類型,一個 key 對應一個 value。
二進制安全是指在傳輸數據時,編碼、解碼發生在客戶端,保證二進制數據的信息安全,不會被篡改或者破譯。
而 String 類型是二進制安全的,也就是說可以保存任何的數據,如圖片或者序列化的對象等。
命令
命令 | 作用 |
---|---|
SET key value | 設置給定 key 的值,如果已經存在則覆蓋 |
SETNX key value | 只在 key 不存在時設置 key 的值 |
MSET key value [key value] | 同時設置多個 key-value |
GET key | 獲取指定 key 的值,不存在返回 nil |
GETRANGE key start end | 獲取獲取指定 key 中字符串的子字符串 |
GETBIT key offset | 對 key 所儲存的字符串值,獲取指定偏移量上的位 |
MGET key1 [key2] | 獲取多個給定 key 的值 |
GETSET | 設置指定 key 的值,返回舊值 |
STRLEN key | 返回 key 所儲存的字符串值的長度 |
DEL key | 刪除指定的 key |
INCR key | 將 key 中儲存的數字加1,如果不存在則初始化爲0再操作 |
INCR key 增量 | 指定 key 的自增量 |
DECR key | DECR key 減量 |
APPEND key value | 在指定 key 值的末尾追加 |
雖然 String 可以保存任何數據,但我們還是需要選擇合適的儲存類型,這樣才便於我們操作數據。
String 類型的應用場景:
- 單個字符串或 JSON 字符串數據
- 圖片文件
- 計數器(自增自減命令都具有原子特性)
Hash
相比於 String 類型,Hash 類型更適合儲存一個 JavaBean 對象。
User(id,name,age,sex)
命令
命令 | 作用 |
---|---|
HSET key field/value | 爲指定 key 指定field value |
HMSET key field value [field value] | 同上(多個) |
HGET key field | 獲取儲存在 hash 中的值,根據 field 得到 value |
HMGET key field[field1] | 同上(多個) |
HGETALL key | 返回 hash 表中所有的字段和值 |
HKEYS key | 獲取所有哈希表中的字段 |
HLEN key | 獲取哈希表中字段的數量 |
HDEL key field[field1] | 刪除一個或多個 hash 表字段 |
HSETNX key field value | 只有在字段 field 不存在時,設置哈希表字段的值 |
HINCRBY key field increment | 爲 hash 表 key 中的指定整數字段加上增量 |
HINCRBYFLOAT key field increment | 同上(浮點字段) |
HEXISTS key field | 查看 hash 表 key 中,指定的字段是否存在 |
爲什麼不用 String 存儲一個對象?
首先我們來看下,用 String 類型來儲存一個用戶對象的方式:
- 將用戶 ID 作爲查找 key,而其他信息以序列化的方式存儲,在操作數據時,需要進行序列化或反序列化,同時需要考慮併發問題。
- 將對象所有的成員都以 key-value 的形式存儲,用 ID+屬性名來作爲 key,這樣雖然沒有了上一種方法的缺點,但造成大量內存的浪費。
反觀hash 類型,它是最接近關係數據庫結構的數據類型,可以將數據庫一條記錄轉換成一個 hashmap 存放到 redis 中。
Java 操作 Redis
常用的兩種:
- Jedis是Redis官方推薦的面向Java的操作Redis的客戶端
- RedisTemplate 是 SpringDataRedis中對JedisApi 的高度封裝
Jedis
1.引入 JAR
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.4.2</version>
</dependency>
2.連接 Redis
//直接 new Jedis 對象
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.auth("123456");
//通過 redis pool
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(5);//最大連接數
poolConfig.setMaxIdle(1);//最大空閒數
String host = "127.0.0.1";
int port = 6379;
JedisPool pool = new JedisPool(poolConfig,host,port);
Jedis jedis = pool.getResource();
jedis.auth("123456");
3.使用 Jedis 客戶端
String key = "moke";
if(jedis.exists(key)){
String res = jedis.get(key);
System.out.println("Redis數據:"+res);
}else{
//查詢數據庫,獲取到 res
String res = "數據庫數據";
jedis.set(key,result);
System.out.println("MySQL數據:"+res);
}
jedis.close();
這一步就是我們平時在項目中的應用,爲了減輕數據庫的訪問壓力,首先判斷 Key 是否存在,如果存在則直接返回;如果不存在,才查詢數據庫,然後將結果存到 redis 中,下次調用就不需要訪問數據庫。
當然我們可以將獲取 Jedis 客戶端的操作封裝成一個工具類:
public class JedisUtils{
private static final JedisPool JEDIS_POOL;
static{
JedisPoolConfig config = new JedisPoolConfig();
poolConfig.setMaxTotal(5);
poolConfig.setMaxIdle(1);
JEDIS_POOL = new JedisPool(poolConfig,"127.0.0.1",6379);
}
public static Jedis getJedis(){
Jedis jedis = JEDIS_POOL.getResource();
jedis.auth("123");
return jedis;
}
}
我們用上面的工具類,來看下如何使用 Jedis 來操作 hash 類型:
//user = selectById(id)
Jedis jedis = JedisUtils.getJedis();
String key = user.getName() + user.getId();
if(jedis.exist(key)){
Map<String,String> hash = jedis.hgetAll(key);
User u = new User();
u.setId(Integer.parseInt(hash.get("id")));
u.setName(hash.get("name"));
u.setAge(hash.get("age"));
u.setSex(hash.get("sex"));
System.out.println("Redis中數據"+u);
}else{
User user = userService.getBy(id);
Map<String,String> hash = new HashMap<String,String>();
hash.put("id",u.getId()+"");
hash.put("name",u.getName());
hash.put("age",u.getAge());
hash.put("sex",u.getSex());
jedis.hmset(key,hash);
System.out.println("MySQL中數據"+user);
}
從上面我們可以發現,jedis 操作 Redis 的方法名其實就是我們之前學的那些 Redis 的命令。
也可以發現用 Jedis 操作比較繁瑣,所以就有了 RedisTemplate 模板,封裝了 redis 連接池管理的邏輯,無需關心連接的獲取與釋放。
RedisTemplate
使用過程可以在之前的文章中看:地址
這裏主要說下在 Spring 中配置 redistemplate 時,配置 key 或者 value 的序列化,不然可能會亂碼:
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
List
類似於 LinkedList,主要用於存儲一組數據,可以很方便實現分頁,也可以用於實現任務(消息)隊列。
命令
命令 | 作用 |
---|---|
LPUSH key value [value1] | 將一個或多個值插入到列表頭部 |
RPUSH key value [value1] | 將一個或多個值插入到列表尾部 |
LPUSHX key value | 將一個值插入到已存在的列表頭部,如果不存在則無效 |
RPUSHX key value | 將一個值插入到已存在的列表尾部,如果不存在則無效 |
LLEN key | 獲取列表長度 |
LINDEX key index | 通過所以獲取列表中的元素 |
LRANGE key start stop | 獲取列表指定範圍內的元素 |
LPOP key | 移除並獲取列表的第一個元素 |
RPOP key | 移除並獲取列表的最後一個元素 |
BLPOP key1 [key2] timeout | 移除並獲取列表第一個元素,如果列表沒有元素會阻塞到有或者超時 |
LTRIM key start stop | 對一個列表進行修剪,不在區間的元素刪去 |
LSET key index value | 通過索引設置列表元素的值 |
LINSERT key BEFORE | AFTER wordl value |
RPOPLPUSH source detination | 移除列表最後一個元素並添加到另一個列表返回 |
BRPOPLPUSH source destination timeout | 從列表中彈出一個值並插入另一個列表,如果前一個列表沒有元素則會阻塞到有或超時 |
任務隊列
list 一般會用來實現一個消息隊列
任務隊列:
在處理請求時,某些操作的執行時間可能會比較長,爲了避免用戶一直等待,通過將其放入隊列,並在之後對隊列進行處理,這種將工作交給任務處理器來執行的做法被稱爲任務隊列。
而任務隊列則可以使用 BRPOPLPUSH 命令來實現。
Set
類似 Java 中的 HashTable 集合,是 String 類型的無序集合,集合成員是唯一的。
Set 底層存儲結構使用了 intset 和 hashtable 兩種,前者爲數組而後者則是哈希表:
- intset 只有保存整數元素時才使用,使得它可以通過二分查找元素。
- hashtable 的value永遠爲null,實際就是通過計算hash的方式來快速排重的,這也是set能提供判斷一個成員是否在集合內的原因。
命令
命令 | 作用 |
---|---|
SADD key member1 [member2] | 向集合添加一個或多個成員 |
SCARD key | 獲取集合的成員數 |
SMEMBERS key | 返回集合中的所有成員 |
SISMEMBER key member | 判斷 member 元素是否是集合 key 的成員 |
SRANDMEMBER key[count] | 返回集合中一個或多個隨機數 |
SREM key member1 [member2] | 移除集合中一個或多個成員 |
SPOP key[count] | 移除並返回集合中的一個隨機元素 |
SMOVE source destination member | 將 member 元素移到 destination 集合 |
SDIFF key1 [key2] | 返回給定所有集合的差集(左側) |
SDIFFSTORE destination key1 [key2] | 結果存儲在 destination 中 |
SINTER key1 [key2] | 返回給定所有集合的交集(共有) |
SINTERSTORE destination key1 [key2] | 結果存儲在 destination 中 |
SUNION key1 [key2] | 返回所有給定集合的並集 |
SUNION STORE destination key1 [key2] | 結果存儲在 destination 中 |
應用場景:
- 集合運算:共同好友、共同關注等
- 唯一性:統計訪問網站的IP
ZSet
ZSet,即 sorted set,因爲 redis 中有序集合的操作命令都以 z 開頭。
ZSet 和 Set 不同的是每個元素都會關聯一個 double 類型的分數,通過分數對集合中的成員進行從小到大的排序。
有序集合的成員是唯一的,但分數(score)可以重複。
命令
命令 | 作用 |
---|---|
ZADD key score1 member1 [score2 member2] | 向有序集合添加一個或多個成員,或更新已存在成員的分數 |
ZCARD key | 獲取有序集合的成員數 |
ZCOUNT key min max | 計算指定區間的成員數 |
ZRANK key start stop | 從低到高返回指定索引區間的成員 |
ZREVRANGE key start stop | 從高到低返回指定索引區間的成員 |
del key | 移除集合 |
ZREMRANGEBYRANK key start stop | 移除有序集合中給定的排名區間的所有成員 |
ZREMRANGEBYSCORE key min max | 移除有序集合中給定的分數區間的所有成員 |
應用場景:排行榜、定時任務
Redis 發佈訂閱
發佈/訂閱模式(Pub/Sub)是一種消息模式,它有 兩個參與者 : 發佈者和訂閱者 。發佈者向 某個信道發佈一條消息,訂閱者綁定這個信道,當有消息發佈至信道時就會接收到一個通知。最重要的一點是, 發佈者和訂閱者是完全解耦的,彼此並不知曉對方 的存在。兩者僅僅共享一個信道名稱。
從定義上可以看出,發佈訂閱模式裏雙方是完全解耦的,而在觀察者模式裏,目標對象管理這觀察者,雙方是耦合的,這是最主要的區別,而在發佈訂閱模式中多了一箇中間層信道(頻道)。
命令
命令 | 作用 |
---|---|
SUBSCRIBE chaannel […] | 訂閱給定的一個或多個頻道的信息 |
PSUBSCRIBE pattern […] | 訂閱一個或多個符合給定模式的頻道 |
PUBLISH channel message | 將消息發送到指定頻道 |
UNSUBSCRIBE channel […] | 退訂給定的頻道 |
PUNSUBSCRIBE pattern […] | 退訂給定模式的頻道 |
應用場景:構建實時消息系統,如即時聊天。
Redis 多數據庫
Redis 下,數據庫是由一個整數索引標識,默認情況下,客戶端連接到數據庫 0。
在 Redis 配置文件中:
database 16
表示從 0 開始有16個數據庫
命令
命令 | 作用 |
---|---|
select 索引 | 切換數據庫 |
move key 索引 | 移動數據 |
flushdb | 清除當前數據庫的所有 key |
flushall | 清除整個 Redis 的數據庫所有的 key |
Redis 事務
事務執行的階段:
- 開始事務
- 命令入隊
- 執行事務
Redis 會將一個事務中的所有命令序列化,然後按順序執行,執行中不會被其他命令插入。
命令
命令 | 作用 |
---|---|
DISCARD | 取消事務 |
EXEC | 執行所有事務塊內的命令 |
MULTI | 標記一個事務塊的開始 |
UNWATCH | 取消 WATCH 命令對所有 key 的監視 |
WATCH key […] | 監視 key,如果事務執行前這個 key 被其他命令改動,那麼事務將被打斷 |
事務的錯誤處理
- 語法(報告)錯誤,整個事務不會執行
- 如果執行的某個命令報出錯誤,則只有報錯的命令不會被執行,而其他命令依然執行,不會回滾。
Redis 數據淘汰
當內存不足時,Redis 會根據配置的緩存策略淘汰部分 Keys,以保證寫入成功。如果無淘汰策略或沒找到合適淘汰的 Key 時,會返回 out of memory 錯誤。
最大緩存配置
在配置文件中:
maxmemory 1G
6 種淘汰策略
- volatile-lru:從已設置過期時間的數據集中挑選最近最少使用的數據淘汰。
- volatile-lfu:從已設置過期的 Keys 中,刪除一段時間內使用次數最少的。
- volatile-ttl:從已設置過期時間的數據集中挑選最近要過期的數據淘汰。
- volatile-random:從已設置過期時間的數據集中隨機選擇數據淘汰。
- allkeys-lru:從數據集中挑選最近最少使用的
- allkeys-lfu:從所有 Keys 中刪除一段時間內使用次數最少的。
- allkeys-random:從數據集中隨機選擇數據淘汰。
- no-enviction:禁止淘汰數據,返回錯誤信息
注: 平時使用 Redis 時儘量主動設置 key 的 expire 時間,有助於提高查詢性能。
Redis 持久化
兩種持久化機制:
- RDB
- AOF
RDB
redis 默認的持久化機制,以快照的方式,將內存中的數據寫入二進制文件中(dump.rdb)
優點: 保存數據和恢復數據極快,適用於災難備份。
缺點:小內存機器不適合,且 RDB 只有符合要求才會執行快照。
快照條件:
- 服務器正常關閉
- 配置文件中設置:
save 900 1 //每900秒至少一個key發送變化,則產生快照
AOF
RDB 有一個致命缺陷,就是如果 Redis 意外 down 掉的話,就會丟失最後一次快照之後的所有修改。
而 AOF(Append-only file)是在 Redis 的每次寫操作都通過 write 方法將數據追加到文件中,當 Redis 重啓時就會通過重新執行文件中保存的寫命令來在內存中重建整個數據庫的內容。
三種方式:
- appendonly yes:啓用 aof 持久化方式
- appendfsync always:收到寫命令就立即寫入磁盤,保存完全的持久化,最慢
- appendfsync everysec:每秒寫入磁盤一次,折中
- appendfsync no:最快,完全依賴系統
缺點:持久化的文件會越來越大,且對於自增命令,出現多少次自增就會有多少條命令。
Redis 緩存與數據庫一致性
四種解決方案:
- 實時同步
- 異步隊列
- 第三方同步工具(阿里)
- UDF 自定義函數
實時同步
原理
查詢不到緩存時纔會從數據庫查詢,並保存到緩存。而在更新緩存時,先更新數據庫,再將緩存的設置過期時間。
使用註解:
@Cacheable @CachePut @CacheEvict @Caching
緩存穿透
在查詢一個一定不存在的數據,由於緩存在不命中時需要查詢數據庫,查不到數據則不寫入緩存,這就會導致每次請求查詢這個不存在的資源時,每次都要查詢數據庫,造成緩存穿透。
解決:查詢不到不是不緩存,而是緩存空結果(注意insert)。
異步隊列
對於併發程度較高的,可採用異步隊列的方式同步,比如 kafka、ActiveMQ等消息中間件處理消息生產和消費。
第三方同步工具
使用阿里的 canal 實現方式是模擬 mysql slave 和 master 的同步機制(主從複製),通過監控 DB bitlog 的日誌更新來觸發緩存的更新:
熱點 key(緩存雪崩)
對於某個 key 訪問平凡,即使設置了失效時間,在失效時有大量線程來構建緩存,導致負載增加,解決辦法:
- 構建緩存的地方使用鎖,單機用 synchronized,lock等,分佈式用分佈式鎖。
- 緩存過期時間不設置,將時間設置在value中,如果檢查到value中的時間過期則異步更新緩存。
- 設置標籤緩存以及它的過期時間,該標籤過期後會異步更新實際緩存。
Redis 併發
一般不會使用一臺 Redis 服務器,原因:
- 單個 Redis 服務器容易發生單點故障
- 單個服務器的性能與系統資源比較有限
高可用
“高可用性”通常來描述一個系統經過專門的設計,從而減少停工時間,而保證其服務的高度可用性。
高併發
高併發是指通過設計保證系統能夠同時並行處理很多請求。
關鍵字
- 響應時間:系統對請求做出響應的時間
- 吞吐量:單位時間內處理的請求數
- 每秒查詢率:每秒響應請求數
- 併發用戶數:同時承載正常使用系統功能的用戶數量
提高併發能力
主要有兩種類型:
- 垂直擴展
- 水平擴展
垂直擴展
垂直擴展就是提升單機處理能力,例如:
- 增加單機硬件性能:CPU、內存、網卡…
- 提升單機架構性能:緩存減少 IO 次數,使用異步來增加吞吐量,使用無鎖數據結構減少響應時間
優點:
是最快最簡單的方式。
缺點:
提升是有限的
水平擴展
水平擴展則是隻要增加服務器的數量,就能線性擴充系統性能,主要難點是在架構各層進行可水平擴展的設計。
redis 是一個非關係型數據庫,其常見的水平擴展也和 mysql 一樣,可以實現主從複製,如下圖:
將一臺 Redis 服務器作爲主庫,其他三臺作爲從庫,主庫只負責寫數據,每次有數據更新將更新的數據同步到它所有的從庫,而從庫只負責數據。
注: 一臺主庫可以有多個從庫,而一個從庫只能附屬一臺主庫。
主從複製配置
1.主數據庫不需要配置,只需在傳講從數據庫時指定主服務器就行了
port 6380 //從服務器的端口號
slaveof 127.0.0.1 6379 //指定主服務器
也可以在啓動時指定:
./usr/local/redis/redis.server ./redis.conf --port 6380 --slaveof 127.0.0.1
2.主從服務器客戶端切換
slaveof on one //變回主
slaveof ip port //變回從
Redis 集羣
常見的 Redis 集羣搭建方案有三種:
- Twitter 的 twemproxy
- 豌豆莢的 codis
- redis 官方的 redis-cluster
(至少3master+3slave)
這裏我們主要學習的 redis 官方的 redis-cluster,其主要特點:
- 所有節點彼此互聯,內部使用二進制協議優化傳輸速度和帶寬。
- 節點的 fail 狀態是檢測到集羣中超過半數的節點無法連接纔會生效。
- 客戶端與 節點直連,不需要中間代理層,只需連接其中一個節點。
- redis-cluster 把所有物理節點映射到[0-16383]slot上,由它負責維護。
16384個哈希槽,對 key 使用 crc16 算法,再對16384求餘數,通過這樣的方式將內容放到對應的槽中
容錯性
redis-cluster 投票機制,投票過程是集羣中所有 master 參與,如果半數以上 master 與某個 master 節點通信超時,則認爲當前 master 節點掛掉。
如果集羣超過半數以上 master 掛掉,或者任意master掛掉且其沒有slave,則集羣進入 fail 狀態。
集羣搭建
集羣中至少有奇數個節點,所以搭建集羣最少需要 3 臺主機,同時每個節點至少有一個備份節點,所以最少需要創建使用 6 臺機器才能完成 Redis Cluster 集羣(主節點、備份節點由 redis-cluster 集羣確定)。
1.對每個機器中的 redis 的配置文件進行修改:
2.創建集羣
可以通過官方提供的 redis-trib.rb 來創建集羣,安裝後直接:
redis-trib.rb create --replicas 1 197.168.1.101:7001 197.168.1.102:7002 197.168.1.103:7003 197.168.1.104:7004 197.168.1.105:7005 197.168.1.106:7006
3.連接集羣
redis-cli -h 197.168.1.103 -c -p 7003
只需要指定連接集羣上的其中一個節點即可。
4.查看集羣信息
Cluster Nodes
Cluster Info
每個 Redis 的節點都有一個 ID 值,被稱爲節點 ID。此 ID 被特定的 redis 實例永久使用,以便實例再集羣上下文中具有唯一的名稱。