文章目录
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);
}
}
}