文章目錄
Redis在mac下的安裝
1、安裝homebrew
2、brew install redis
3、進入 /usr/local/etc redis-server redis.conf redis就啓動起來了
4、redis-cli -h 127.0.0.1 -p 6379 客戶端進行連接,開始操作redis
Redis五大基礎數據結構
5種數據結構分別是:
string(字符串)、list(列表)、hash(字典)、 set(集合) 和 zset(有序集合)
redis所有的數據結構都是唯一的key值來獲取相應的value值,不同類型的數據結構差異就在與value的結構不同。
1、String(字符串)
String是redis最簡單的數據結構,它的內部結構其實就是“字符數組”。
內部結構實現
內部結構的實現類似於java的LinkedList,採用動態字符串,預分配空間的方式來減少內存空間的頻繁分配。當前字符串分配的實際空間 c叩acity一般要高於實際字符串長度 len。當字符 串長度小於 lMB 肘,擴窯都是加倍現有的空間 。如果字符串長 度超過 lMB,擴窯時 一次只會多擴! MB 的空間 。需要注意的 是字符串最大長度爲 512MB。
命令使用
> set name helloworld
OK
> get name
helloworld
> exists name
(integer) 1
> del name
(integer) 1
> get name
(nil)
// 批量鍵值對
> mset nam1 xiaoming name2 zhangsan name3 lisi
> mget name1 name2 name3
> (1) xiaoming
> (2) zhangsan
> (3) lisi
// 設置過期時間
> setex name 5 helloworld # 5s 之後過期,等價於 set + expire
> setnx name helloworld # 如果name不存在,則set創建,Set If Not Exists
> (integer)1
> set name 5 hahaha
> (integer)0 # 因爲name已經存在,所以set創建不成功
2、list(列表)
Redis列表相當於java裏面的LinkedList,但是它是鏈表,而不是數組。
當列表彈出了最後一個元素之後,該數據結構被自動刪除,內存被回收。
因此,redis的list結構可以被用來當做隊列進行使用。
redis的list結構經常會被用來做異步隊列進行使用。將需要延後處理的任務結構體系序列化爲字符串,塞進redis列表,另外一個線程去輪詢的處理數據即可。
在頭部插入數據
**lpush key value ** 自己方便記憶將 “L” 理解成 list,從list集合的開始插入數據
在尾部插入數據
**rpush key value ** “r” 理解成 result,在list的最後面開始插入數據
右邊進,左邊出:隊列
127.0.0.1:6379> rpush key value [value ...]
127.0.0.1:6379> rpush nam1 aa bb cc
(integer) 3
127.0.0.1:6379> lpop nam1
"aa"
127.0.0.1:6379> lpop nam1
"bb"
127.0.0.1:6379> lpop nam1
"cc"
127.0.0.1:6379> llen nam1 獲取隊列的長度
(integer) 0
// 右邊進,右邊出,棧
127.0.0.1:6379> rpush nam1 aa bb cc
(integer) 3
127.0.0.1:6379> rpop nam1
"cc"
127.0.0.1:6379>
127.0.0.1:6379> rpop nam1
"bb"
127.0.0.1:6379> rpop nam1
"aa"
3、hash(字典)
hash字典相當於Java裏面的HashMap,存儲結構是跟HashMap一樣,採用“數組 + 鏈表”結構進行存儲。
與Java的HashMap的區別是,
- 1.redis的字典值只能存儲字符串
- 2.它們的rehash的方式不同,
java的hashMap 是一次性rehash,耗時較長,redis的hash採用的是漸進式hash。
127.0.0.1:6379> hset name5 java hashMap 存儲 key 爲name5的 field value
(integer) 1
127.0.0.1:6379> hget name5 java 獲取制定key的field value
"hashMap"
127.0.0.1:6379> hset name5 java wahahaha 返回0表示 field 已經存在,用新值覆蓋舊值
(integer) 0
127.0.0.1:6379> hget name5 java 獲取制定key的field value,發現新值已經將舊值覆蓋
"wahahaha"
127.0.0.1:6379> hmset name6 hoodoop1 spark hoodoop2 reduce 同時保存多個value
OK
127.0.0.1:6379> hgetall name6 獲取指定key對應的值 entries[],key和value間隔出現
1) "hoodoop1"
2) "spark"
3) "hoodoop2"
4) "reduce"
127.0.0.1:6379> hdel name5 java
(integer) 1
-
擴容,縮容機制
Java 中的 HashMap 有擴容的概念,當 LoadFactor 達到閏值時,需要重新分配一個新的 2 倍大小的數組,然後將所有的元素全部 rehash 掛到新的數組下面。 rehash 就是將元素的 hash 值對數組長度進行取模運算,因爲長度變了,所以每個元素掛接 的槽位可能也發生了變化。又因爲數組的長度是 2 的 n 次方,所以取模運算等價於 位與操作。 -
漸進式rehash
Java 的 HashMap 在擴容時會一次性將舊數組下掛接的元素全部轉移到新數組下 面。如果 HashMap 中元素特別多,線程就會出現卡頓現象。 Redis爲了解決這個問題, 採用“漸進式 rehash。
它會同時保留舊數組和新數組,然後在定時任務中以及後續對 hash 的指令操作 申漸漸地將舊數組中掛攘的元素遷移到新數組上。這意昧着要操作處於 rehash 中的 字典,需要同時訪問新舊兩個數組結構。如果在舊數組下面找不到元素,還需要去 新數組下面尋找。
4、set集合
Redis 的集合相當於 Java 語言裏面的 HashSet,它內部的鍵值對是無序的、唯一 的。它的內部實現相當於一個特殊的字典,字典中所有的 value 都是一個值NULL。
當集合中最後一個元素被移除之後,數據結構被自動刪除,內存被回收。
127.0.0.1:6379> sadd name6 1111 2222
(integer) 2
127.0.0.1:6379> sadd name6 3333
(integer) 1
127.0.0.1:6379> smembers name6
1) "1111"
2) "2222"
3) "3333"
127.0.0.1:6379> sismember name6 1111 查詢某個key是否存在,返回 1則存在, 返回0則不存在
(integer) 1
127.0.0.1:6379> spop name6 取出來一個,按照順序取的
"1111"
127.0.0.1:6379> smembers name6
1) "2222"
2) "3333"
5、zset集合
zset 類似於 Java的 SortedSet和 HashMap 的結合體, 方面它是個 set,保證 了內部 value 的唯性,另方面它可 以給每個 value 賦予一個 score,代表 這個 value 的排序權重。它的內部實現 用的是一種叫作“跳躍列表”的數據 結構。
例如:
zset 可以用來存儲粉絲列表, value 值是粉絲的用戶 ID, score 是關注時間。我
們可以對粉絲列表按關注時間進行排序。
zset 還可以用來存儲學生的成績, value 值是學生的 ID, score 是他的考試成績。
我們對成績按分數進行排序就可以得到他的名次。
zadd 進行添加的時候,scores是用來進行排序的。
因此,zadd key score value value 是不可重複的
127.0.0.1:6379> zadd score 50 333
(integer) 1
127.0.0.1:6379> zadd score 50 222
(integer) 1
127.0.0.1:6379> zadd score 70 111
(integer) 1
127.0.0.1:6379> zadd score 70 555
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379> zadd score 30 444
(integer) 1
127.0.0.1:6379> zrange score 0 -1 // 取出所有的元素,按照分數進行排序輸出,成績小的在最前面
1) "444"
2) "222"
3) "333"
4) "111"
5) "555"
127.0.0.1:6379> zcard score // 取出元素總數
(integer) 5
127.0.0.1:6379> zrevrange score 0 -1 按照成績 "逆序" 列出,成績最大的在前面
1) "555"
2) "111"
3) "333"
4) "222"
5) "444"
127.0.0.1:6379> zscore score 111 // 取出指定 value 的 score
"70"
127.0.0.1:6379> zrank score 111 // 查詢指定成員的排名
(integer) 3
127.0.0.1:6379> zrangebyscore score 0 50 // 查詢0-50之間有哪些人
1) "444"
2) "222"
3) "333"
127.0.0.1:6379> zrem score 111 // 刪除 score
(integer) 1
redis的過期時間,過期策略
我們在往redis中保存數據的時候,會給key設置過期時間;那麼在設置的過期時間之後,redis通過採用 “定期刪除 + 惰性刪除”的方式進行刪除。
定期刪除:指的是redis會定期,隨機的抽取key,進行檢查,key的時間有沒有過期。
惰性刪除:指的是 應用在 根據 key 獲取 value 的時候,redis會檢查key是否過期,如果過期,就什麼都不返回
redis的內存淘汰策略
容器型數據結構的通用規則
list、 set、 hash、 zset 這四種數據結構是容器型數據結構
它們共享下面兩條通用規則:
-
create if not exists:如果容器不存在,那就創建一個,再進行操作。比如rpush操作剛開始是沒有列表的, Redis就會自動創建一個,然後再 rpush進去新元素。
-
drop if no elements:如果容器裏的元素沒有了,那麼立即刪除容器,釋放內存。這意昧着!pop 操作到最後一個元素,列表就消失了。
Redis分佈式鎖
在分佈式應用中,我們經常會遇到
public abstract class LockTemplate<T> {
private JedisClient jedisClient;
/**
* 最小鎖超時時間
*/
private int minLockExpiredSeconds = 1;
/**
* 當前請求重試次數
*/
private int loopCount = 0;
/**
* 最大重試次數
*/
private static final int MAX_REPEAT_COUNT = 1;
/**
* setnx成功狀態
*/
private static final int SETNX_SUCC_STATUS = 1;
private static final String LOCK_VALUE_SEPARATOR = "##";
private static final String LOCK_TEMPLATE_SETNX_SUCC = "lock_template_setnx_succ";
private static final String LOCK_TEMPLATE_SETNX_FAIL = "lock_template_setnx_fail";
private static final String LOCK_TEMPLATE_EXPIRED_SUCC = "lock_template_expired_succ";
private static final String LOCK_TEMPLATE_LOCK_EXPIRED = "lock_template_lock_expired";
private static final String LOCK_TEMPLATE_LOCK_UNEXPIRED = "lock_template_lock_unexpired";
private static final String LOCK_TEMPLATE_GETSET_SUCC = "lock_template_getset_succ";
private static final String LOCK_TEMPLATE_GETSET_FAIL = "lock_template_getset_fail";
private static final String LOCK_TEMPLATE_PARAM_ERROR = "lock_template_param_error";
private static final String LOCK_TEMPLATE_LOCK_ERROR = "lock_template_lock_error";
private static final String LOCK_TEMPLATE_UNKNOWN_ERROR = "lock_template_unknown_error";
private static final String LOCK_TEMPLATE_RELEASE_LOCK_SUCC = "lock_template_release_lock_succ";
private static final String LOCK_TEMPLATE_RELEASE_LOCK_FAIL = "lock_template_release_lock_fail";
/**
* 這個監控要注意,刪除鎖失敗,可能因爲業務執行時間超過了鎖的過期時間,需要排查
*/
private static final String LOCK_TEMPLATE_RELEASE_LOCK_GET_VALUE_NULL = "lock_template_release_lock_get_value_null";
/**
* 構造參數
*
* @return
*/
protected abstract LockParam buildLockParam();
/**
* 加鎖成功後處理業務邏輯
*
* @return
*/
protected abstract T lockSucc();
/**
* 加鎖失敗後處理業務邏輯
* 可以根據當前鎖返回的值,做業務處理,如冪等
*
* @param lastValue
* @return
*/
protected abstract T lockFail(String lastValue);
/**
* 執行邏輯, 鎖默認超時時間1秒
*
* @param jedisClient
* @return
*/
public T execute(JedisClient jedisClient) {
this.jedisClient = jedisClient;
T result = null;
try {
result = doExecute();
} catch (CheckParamException e) {
LOCK_TEMPLATE_PARAM_ERROR;
throw e;
} catch (LockException e) {
LOCK_TEMPLATE_LOCK_ERROR;
throw e;
} catch (Exception e) {
LOCK_TEMPLATE_UNKNOWN_ERROR;
throw e;
}
return result;
}
private T doExecute() {
// 獲取參數
LockParam lockParam = buildLockParam();
// 驗證參數
checkParam(lockParam);
// 獲取當前lock值
String currentLockValue = buildLockValue(lockParam);
// 加鎖
if (jedisClient.setnx(lockParam.getKey(), currentLockValue) == SETNX_SUCC_STATUS) {
LOCK_TEMPLATE_SETNX_SUCC;
// 加鎖成功
return wrapperLockSucc(lockParam);
}
LOCK_TEMPLATE_SETNX_FAIL;
log.warn("加鎖失敗, 開始補償流程!key: {}, value: {}", currentLockValue, lockParam.getValue());
// 加鎖失敗,判斷鎖是否過期,解決沒有expire的問題
String existLockValue = jedisClient.get(lockParam.getKey());
log.info("獲取到redis中的值, existLockValue: {}", existLockValue);
if (existLockValue == null) {
return retryLock();
} else {
// 鎖未過期
LOCK_TEMPLATE_LOCK_UNEXPIRED;
String[] arr = StringUtils.split(existLockValue, LOCK_VALUE_SEPARATOR);
long lastLockTime = Long.parseLong(arr[0]);
String lastValue = String.valueOf(arr[1]);
if (lastLockTime < System.currentTimeMillis()) {
// 進入當前邏輯,證明之前獲取鎖的線程setnx後設置expired失敗
// 鎖已過期,未設置過期時間
// getset防止併發
String currentNowValue = jedisClient.getSet(lockParam.getKey(), currentLockValue);
if (existLockValue.equals(currentNowValue)) {
LOCK_TEMPLATE_GETSET_SUCC;
log.info("通過getSet方式獲取到鎖. currentNowValue: {}", currentNowValue);
return wrapperLockSucc(lockParam);
} else {
LOCK_TEMPLATE_GETSET_FAIL;
log.warn("通過getSet方式未獲取到鎖. existLockValue: {}, currentNowValue: {}", existLockValue, currentNowValue);
return lockFail(currentNowValue);
}
} else {
log.info("鎖未過期,返回緩存值, existLockValue: {}", existLockValue);
// 鎖未過期,返回緩存值
return lockFail(lastValue);
}
}
}
/**
* 鎖過期,重試加鎖
*
* @return
*/
private T retryLock() {
// 鎖已過期,重試一次
LOCK_TEMPLATE_LOCK_EXPIRED;
log.info("鎖過期,進入重試.");
if (loopCount <= MAX_REPEAT_COUNT) {
loopCount++;
return doExecute();
} else {
throw new LockException("重試後未獲取到鎖");
}
}
private T wrapperLockSucc(LockParam lockParam) {
try {
// 設置過期時間
jedisClient.expire(lockParam.getKey(), getExpiredSeconds(lockParam));
LOCK_TEMPLATE_EXPIRED_SUCC;
return lockSucc();
} finally {
releaseLock(lockParam.getKey());
}
}
private void checkParam(LockParam lockParam) {
ParamPreconditions.notEmpty(lockParam.getKey(), "key不能爲空");
ParamPreconditions.notEmpty(lockParam.getValue(), "value不能爲空");
ParamPreconditions.checkArgument(lockParam.getExpiredSeconds() <= TimeUnit.DAYS.toSeconds(1),
"redis鎖時間不能大於1天");
ParamPreconditions.checkArgument(lockParam.getExpiredSeconds() >= minLockExpiredSeconds,
"redis鎖時間必須大於" + minLockExpiredSeconds + "秒");
}
/**
* 獲取過期時間, 單位秒
*
* @param lockParam
* @return
*/
private int getExpiredSeconds(LockParam lockParam) {
int expiredSeconds = lockParam.getExpiredSeconds();
log.info("獲取到鎖超時時間: {}s", expiredSeconds);
return expiredSeconds;
}
/**
* 構建緩存值, value: timestamp#lockParam.value
*
* @param lockParam
* @return
*/
private String buildLockValue(LockParam lockParam) {
return new StringBuilder().append(System.currentTimeMillis() + lockParam.getExpiredSeconds() * 1000L)
.append(LOCK_VALUE_SEPARATOR)
.append(lockParam.getValue())
.toString();
}
private void releaseLock(String lockKey) {
if (!buildLockParam().isDeleteLockAfterExecution()) {
log.info("業務執行完後不主動刪除鎖,key: {}", lockKey);
return;
}
try {
Long lockId = jedisClient.del(lockKey);
if (lockId.longValue() == 0L) {
LOCK_TEMPLATE_RELEASE_LOCK_GET_VALUE_NULL;
}
LOCK_TEMPLATE_RELEASE_LOCK_SUCC;
} catch (Exception e) {
log.error("刪除鎖異常, lockKey: {}", lockKey, e);
XMonitor.countBizMetric(LOCK_TEMPLATE_RELEASE_LOCK_FAIL);
}
}
}