​Redis​​之基础知识总结

一、支持的基本的数据类型

1.1. 五大数据类型

​Redis​​​有5个基本数据结构,​​string​​​、​​list​​​、​​hash​​​、​​set​​​和​​zset​​。它们是日常开发中使用频率非常高应用最为广泛的数据结构,

  • String类型
  1. ​String​​​是​​redis​​最基本的类型,一个key对应一个value,​​sring​​类型是二进制安全的。​​redis​​的​​string​​可以包含任何数据。​​Sring​​类型是​​Redis​​最基本的数据类型,一个键最大能存储​​512MB​​。
  2. ​Redis​​​的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于​​Java​​的​​ArrayList​​,采用预分配冗余空间的方式来减少内存的频繁分配。
  3. 内部为当前字符串实际分配的空间​​capacity​​一般要高于实际字符串长度​​len​​。当字符串长度小于​​1M​​时,扩容都是加倍现有的空间,如果超过​​1M​​,扩容时一次只会多扩​​1M​​的空间。
  4. 针对​​String​​的一些操作:
127.0.0.1:6379> set name zhangsan  # 初始化字符串
OK
127.0.0.1:6379> get name # 获取字符串
"zhangsan"
127.0.0.1:6379> strlen name # 字符串长度
(integer) 8
127.0.0.1:6379> getrange name 2 4 # 获取子串 key值和开始和结束位置
"ang"
127.0.0.1:6379> setrange name 2 6666 # 覆盖子串 将会把子串 6666 从2位置开始覆盖
(integer) 8
127.0.0.1:6379> get name # 覆盖之后的子串
"zh6666an"
127.0.0.1:6379> append name 123456789 # 追加子串
(integer) 17
127.0.0.1:6379> get name # 追加之后的子串
"zh6666an123456789"
127.0.0.1:6379>
  1. 将作为计数器使用
127.0.0.1:6379> set index 10                       # 设置 index 值为 10
OK
127.0.0.1:6379> get index
"10"
127.0.0.1:6379> incrby index 1 # 增加一
(integer) 11
127.0.0.1:6379> get index # 增加一后为11
"11"
127.0.0.1:6379> decrby index 1 # 减去一
(integer) 10
127.0.0.1:6379> get index # 减去一位10
"10"
127.0.0.1:6379> incr index # 加一
(integer) 11
127.0.0.1:6379> decr index # 减一
(integer) 10
127.0.0.1:6379>
  1. 过期删除操作
127.0.0.1:6379> expire index 5                     # 设置过期时间
(integer) 1 # 1 成功 0 key不存在
127.0.0.1:6379> ttl index # 获取还有多久过期
(integer) -2 # -2 表示变量不存在, -1 表示设置过期时间 返回正常正数为还有多少秒过期
127.0.0.1:6379> del name # 删除
(integer) 1 # 删除成功返回1
127.0.0.1:6379> get name # 获取
(nil) # b变量不存在
127.0.0.1:6379>
```
  • list类型
  1. ​Redis​​​将列表数据结构命名为​​list​​而不是​​array​​,是因为列表的存储结构用的是链表而不是数组,而且链表还是双向链表。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优。如果​​list​​的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。
  2. 负下标 链表元素的位置使用自然数​​0,1,2,....n-1​​表示,还可以使用负数​​-1,-2,...-n​​来表示,​​-1​​表示「倒数第一」,​​-2​​表示「倒数第二」,那么​​-n​​就表示第一个元素,对应的下标为​​0​​。
  3. 队列/堆栈 链表可以从表头和表尾追加和移除元素,结合使用​​rpush/rpop/lpush/lpop​​四条指令,可以将链表作为队列或堆栈使用,左向右向进行都可以
  4. list的一些操作:
    在日常应用中,列表常用来作为异步队列来使用
127.0.0.1:6379> rpush code go java  # 将值value插入到列表key的表尾,当且仅当key存在并且是一个列表。
(integer) 2
127.0.0.1:6379> rpush code python php c c++
(integer) 6
127.0.0.1:6379> rpop code # 移除并返回列表key的尾元素
"c++"
127.0.0.1:6379> rpop code
"c"
127.0.0.1:6379> lpop code # 移除并返回列表key的头元素
"go"
127.0.0.1:6379> lpop code
"java"
127.0.0.1:6379> lrange code 0 -1 # 查看列表中的元素 0 开始位置, -1为最后一个位置
1) "python"
2) "php"
127.0.0.1:6379> lpush code 12345 # 往表头插入数据
(integer) 3
127.0.0.1:6379> lrange code 0 -1 # 将12345 插入到了表头
1) "12345"
2) "python"
3) "php"
127.0.0.1:6379> lpush code java1 java2 # 插入多个值
(integer) 6
127.0.0.1:6379> lrange code 0 -1
1) "java2"
2) "java1"
3) "rust"
4) "12345"
5) "python"
6) "php"
127.0.0.1:6379> llen code # 获取列表的长度
(integer) 6
127.0.0.1:6379> lindex code 2 # 访问位置2的数据
"rust"
127.0.0.1:6379> lset code 1 js # 修改位置1的数据
OK
127.0.0.1:6379> lrange code 0 -1
1) "java2"
2) "js"
3) "rust"
4) "12345"
5) "python"
6) "php"
127.0.0.1:6379> linsert code after js js1 # 在js数据之后插入数据
(integer) 7
127.0.0.1:6379> lrange code 0 -1
1) "java2"
2) "js"
3) "js1"
4) "rust"
5) "12345"
6) "python"
7) "php"
127.0.0.1:6379> linsert code before js js2 # 在js数据之前插入数据
(integer) 8
127.0.0.1:6379> lrange code 0 -1
1) "java2"
2) "js2"
3) "js"
4) "js1"
5) "rust"
6) "12345"
7) "python"
8) "php"
127.0.0.1:6379> lrem code 1 js # 元素删除
(integer) 1
127.0.0.1:6379> lrange code 0 -1
1) "java2"
2) "js2"
3) "js1"
4) "rust"
5) "12345"
6) "python"
7) "php"
127.0.0.1:6379> ltrim code -4 -1 # 截取定长数据, 其他部分将被舍去
OK
127.0.0.1:6379> lrange code 0 -1
1) "rust"
2) "12345"
3) "python"
4) "php"
127.0.0.1:6379>
  1. 快速列表
    如果再深入一点,你会发现​​Redis​​底层存储的还不是一个简单的​​linkedlist​​,而是称之为快速链表​​quicklist​​的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是​​ziplist​​,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成​​quicklist​​。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是​​int​​类型的数据,结构上还需要两个额外的指针​​prev​​和​​next​​。所以​​Redis​​将链表和​​ziplist​​结合起来组成了​​quicklist​​。也就是将多个​​ziplist​​使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
  • hash类型
  1. 哈希等价于​​Java​​语言的​​HashMap​​或者是​​Python​​语言的​​dict​​,在实现结构上它使用二维结构,第一维是数组,第二维是链表,​​hash​​的内容​​key​​和​​value​​存放在链表中,数组里存放的是链表的头指针。通过​​key​​查找元素时,先计算​​key​​的​​hashcode​​,然后用​​hashcode​​对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的​​value​​值,链表的作用就是用来将产生了「​​hash​​碰撞」的元素串起来。​​Java​​语言开发者会感到非常熟悉,因为这样的结构和​​HashMap​​是没有区别的。哈希的第一维数组的长度也是​​2^n​​。
  2. 一些常规操作:
127.0.0.1:6379> hset code name zhangsan  # 增加一个元素
(integer) 1
127.0.0.1:6379> hmset code lisi threeClass wangwu fourClass # 增加多个元素
OK
127.0.0.1:6379> hget code name # 通过key获取值
"zhangsan"
127.0.0.1:6379> hgetall code # 获取所有的值
1) "name"
2) "zhangsan"
3) "lisi"
4) "threeClass"
5) "wangwu"
6) "fourClass"
127.0.0.1:6379> hkeys code # 获取所有的key
1) "name"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> hvals code # 获取所有的value
1) "zhangsan"
2) "threeClass"
3) "fourClass"
127.0.0.1:6379> hdel code name # 获取key 为name的数据
(integer) 1
127.0.0.1:6379> hgetall code
1) "lisi"
2) "threeClass"
3) "wangwu"
4) "fourClass"
127.0.0.1:6379> hdel code lisi wangwu # 删除多个
(integer) 2
127.0.0.1:6379> hgetall code
(empty list or set)
127.0.0.1:6379> hexists code name # 查看name存在, 存在返回1 不存在返回0
(integer) 1
127.0.0.1:6379> hexists code naaa
(integer) 0

127.0.0.1:6379> hset code name lisi # 计数器,使用,不能使字符串
(integer) 1
127.0.0.1:6379> hincrby code name 1 # 加一操作,这样会报错
(error) ERR hash value is not an integer
127.0.0.1:6379> hset code name 1 # 将name值设置为1
(integer) 0
127.0.0.1:6379> hincrby code name 1 # 加一操作之后就成为 2
(integer) 2
127.0.0.1:6379> hgetall code
1) "name"
2) "2"
127.0.0.1:6379> hdel code name # 删除元素
(integer) 1
127.0.0.1:6379> hexists code name # 是否存在 存在1 不存在0
(integer) 0
127.0.0.1:6379>
```

3. **扩容** 当`hash`内部的元素比较拥挤时(`hash`碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(`rehash`)。如果`hash`结构很大,比如有上百万个键值对,那么一次完整`rehash`的过程就会耗时很长。这对于单线程的`Redis`里来说有点压力山大。所以`Redis`采用了渐进式`rehash`的方案。它会同时保留两个新旧`hash`结构,在后续的定时任务以及`hash`结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

4. **缩容** `Redis`的`hash`结构不但有扩容还有缩容,从这一点出发,它要比`Java`的`HashMap`要厉害一些,`Java`的`HashMap`只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。
  • set类型
  1. Java程序员都知道​​HashSet​​的内部实现使用的是​​HashMap​​,只不过所有的​​value​​都指向同一个对象。​​Redis​​的​​set​​结构也是一样,它的内部也使用​​hash​​结构,所有的​​value​​都指向同一个内部值。
  2. 一些常用的方法:
127.0.0.1:6379> sadd index name sex age height   # 添加元素
(integer) 4
127.0.0.1:6379> smembers index # 列出所有元素
1) "sex"
2) "name"
3) "height"
4) "age"
127.0.0.1:6379> scard index # 统计元素
(integer) 4
127.0.0.1:6379> srandmember index # 随机获取某一元素
"age"
127.0.0.1:6379> srem index sex # 删除元素
(integer) 1
127.0.0.1:6379> spop index # 随机删除一个元素
"height"
127.0.0.1:6379> sismember index sex # 检验某一元素是否存在,0 不存在 1存在
(integer) 0
127.0.0.1:6379> sismember index name
(integer) 1
127.0.0.1:6379>
  • sortset类型
  1. ​SortedSet(zset)​​​是​​Redis​​提供的一个非常特别的数据结构,一方面它等价于​​Java​​的数据结构​​Map​​,可以给每一个元素​​value​​赋予一个权重​​score​​,另一方面它又类似于​​TreeSet​​,内部的元素会按照权重​​score​​进行排序,可以得到每个元素的名次,还可以通过​​score​​的范围来获取元素的列表。
  2. ​zset​​​底层实现使用了两个数据结构,第一个是​​hash​​,第二个是跳跃列表,​​hash​​的作用就是关联元素​​value​​和权重​​score​​,保障元素​​value​​的唯一性,可以通过元素​​value​​找到相应的​​score​​值。跳跃列表的目的在于给元素​​value​​排序,根据​​score​​的范围获取元素列表。
  3. 一些常用操作:
127.0.0.1:6379> zadd info 14 age     # 增加一元素
(integer) 1
127.0.0.1:6379> zadd info 15 name 17 height # 增加多个元素
(integer) 2
127.0.0.1:6379> zcard info # 获取元素个数
(integer) 3
127.0.0.1:6379> zrem info name # 删除元素
(integer) 1
127.0.0.1:6379> zadd info 5 sex
(integer) 1
127.0.0.1:6379> zincrby info 1 sex # 加一操作
"6"
127.0.0.1:6379> zincrby info 3 sex # 加三操作
"9"
127.0.0.1:6379> zrange info 0 -1 # 获取所有元素
1) "lisi"
2) "sex"
3) "age"
4) "height"
127.0.0.1:6379> zrank info sex # 指定元素的正向排名
(integer) 1
127.0.0.1:6379> zrevrank info sex # zrevrank指令获取指定元素的反向排名[倒数第一名]
(integer) 2
127.0.0.1:6379> zscore info sex # 获取元素的权重
"9"
127.0.0.1:6379> zrange info 0 -1 withscores # 携带权重正向输出
1) "lisi"
2) "7"
3) "sex"
4) "9"
5) "age"
6) "14"
7) "height"
8) "17"
127.0.0.1:6379> zrevrange info 0 -1 withscores # 携带权重反向输出
1) "height"
2) "17"
3) "age"
4) "14"
5) "sex"
6) "9"
7) "lisi"
8) "7"
127.0.0.1:6379> zrangebyscore info -inf +inf withscores # 通过zrangebyscore指令指定score范围获取对应的元素列表 -inf 负无穷 +inf 正无穷
1) "lisi"
2) "7"
3) "sex"
4) "9"
5) "age"
6) "14"
7) "height"
8) "17"
127.0.0.1:6379> zrevrangebyscore info +inf -inf withscores # 通过zrevrangebyscore指令获取倒排元素列表
1) "height"
2) "17"
3) "age"
4) "14"
5) "sex"
6) "9"
7) "lisi"
8) "7"
127.0.0.1:6379>

二、简述机制原理

​Redis​​​使用的是​​NIO​​中的多路IO复用的机制,能够非常好的支持并发,从而保持线程安全。

​Redis​​​单线程,也就是底层采用一个线程维护多个不同的客户端​​io​​操作。

但是​​NIO​​​在不同的操作系统上实现的方式有所不同,在我们​​windows​​​操作系统使用​​select​​​实现轮训时间复杂度是为o(n),而且还存在空轮训的情况,效率非常低, 其次是默认对我们轮训的数据有一定限制,所以支持上万的​​tcp​​连接是非常难。

在​​linux​​​操作系统采用​​epoll​​​实现事件驱动回调,不会存在空轮训的情况,只对活跃的 ​​socket​​​连接实现主动回调这样在性能上有大大的提升,所以时间复杂度是为​​o(1)​​。

​nginx​​​、​​redis​​​都能够非常高支持高并发,最终都是​​linux​​​中的​​IO​​​多路复用机制​​epoll​​​, ​​Redis​​​底层采用​​nio epoll​​实现。

三、使用场景

  • Token令牌的生成
  • 短信验证码Code
  • 缓存查询数据
  • 网页计数器
  • 分布式锁
  • 延迟操作

四、 ​​SpringBoot​​整合

4.1. 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 需要引入pools,否则会报类无法找到 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
4.2. 配置文件
# Redis服务器地址
spring.redis.host=192.168.252.135
# Redis数据库索引
spring.redis.database=0
# Redis密码
spring.redis.password=123456
# 端口
spring.redis.port=6379
# 连接池最大连接数,负值无限制
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间 负值无限制
spring.redis.lettuce.pool.max-wait=-1ms
# 关机超时时间
spring.redis.lettuce.shutdown-timeout=100ms
# 最小空闲连接数
spring.redis.lettuce.pool.min-idle=0
4.3. 使用例子
@RestController
public class RedisController {

@Autowired
private StringRedisTemplate stringRedisTemplate;


@GetMapping("/getData")
public String load(@RequestParam("name") String name) {
stringRedisTemplate.opsForValue().set("name", name, TimeUnit.SECONDS.toSeconds(60));
return "success";
}

}
4.4. ​​Redis​​客户端区别
  • ​Jedis​​​:是​​Redis​​的​​Java​​实现客户端,提供了比较全面的​​Redis​​命令的支持,
  • ​Redisson​​​:实现了分布式和可扩展的​​Java​​数据结构。
  • ​Lettuce​​​:高级​​Redis​​客户端,用于线程安全同步,异步和响应使用,支持集群,​​Sentinel​​,管道和编码器。

总体来说:优先使用​​Lettuce​​​,如果需要分布式锁,分布式集合等分布式的高级特性,添加​​Redisson​​​结合使用,因为​​Redisson​​本身对字符串的操作支持很差。