讀Redis深度歷險-基礎數據結構和分佈式鎖

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);
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章