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