Redis
之基础知识总结Redis
有5个基本数据结构,string
、list
、hash
、set
和zset
。它们是日常开发中使用频率非常高应用最为广泛的数据结构,
String类型
String
是redis
最基本的类型,一个key对应一个value,sring
类型是二进制安全的。redis
的string
可以包含任何数据。Sring
类型是Redis
最基本的数据类型,一个键最大能存储512MB
。
Redis
的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java
的ArrayList
,采用预分配冗余空间的方式来减少内存的频繁分配。
内部为当前字符串实际分配的空间capacity
一般要高于实际字符串长度len
。当字符串长度小于1M
时,扩容都是加倍现有的空间,如果超过1M
,扩容时一次只会多扩1M
的空间。
针对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>
将作为计数器使用
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>
过期删除操作
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类型
Redis
将列表数据结构命名为list
而不是array
,是因为列表的存储结构用的是链表而不是数组,而且链表还是双向链表。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优。如果list
的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。
负下标 链表元素的位置使用自然数0,1,2,....n-1
表示,还可以使用负数-1,-2,...-n
来表示,-1
表示「倒数第一」,-2
表示「倒数第二」,那么-n
就表示第一个元素,对应的下标为0
。
队列/堆栈 链表可以从表头和表尾追加和移除元素,结合使用rpush/rpop/lpush/lpop
四条指令,可以将链表作为队列或堆栈使用,左向右向进行都可以
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>
快速列表
如果再深入一点,你会发现Redis
底层存储的还不是一个简单的linkedlist
,而是称之为快速链表quicklist
的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist
,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist
。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int
类型的数据,结构上还需要两个额外的指针prev
和next
。所以Redis
将链表和ziplist
结合起来组成了quicklist
。也就是将多个ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
hash
类型
哈希等价于Java
语言的HashMap
或者是Python
语言的dict
,在实现结构上它使用二维结构,第一维是数组,第二维是链表,hash
的内容key
和value
存放在链表中,数组里存放的是链表的头指针。通过key
查找元素时,先计算key
的hashcode
,然后用hashcode
对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value
值,链表的作用就是用来将产生了「hash
碰撞」的元素串起来。Java
语言开发者会感到非常熟悉,因为这样的结构和HashMap
是没有区别的。哈希的第一维数组的长度也是2^n
。
一些常规操作:
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
类型
Java程序员都知道HashSet
的内部实现使用的是HashMap
,只不过所有的value
都指向同一个对象。Redis
的set
结构也是一样,它的内部也使用hash
结构,所有的value
都指向同一个内部值。
一些常用的方法:
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
类型
SortedSet(zset)
是Redis
提供的一个非常特别的数据结构,一方面它等价于Java
的数据结构Map
,可以给每一个元素value
赋予一个权重score
,另一方面它又类似于TreeSet
,内部的元素会按照权重score
进行排序,可以得到每个元素的名次,还可以通过score
的范围来获取元素的列表。
zset
底层实现使用了两个数据结构,第一个是hash
,第二个是跳跃列表,hash
的作用就是关联元素value
和权重score
,保障元素value
的唯一性,可以通过元素value
找到相应的score
值。跳跃列表的目的在于给元素value
排序,根据score
的范围获取元素列表。
一些常用操作:
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
实现。
SpringBoot
整合<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
# 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
@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";
}
}
Redis
客户端区别Jedis
:是Redis
的Java
实现客户端,提供了比较全面的Redis
命令的支持,
Redisson
:实现了分布式和可扩展的Java
数据结构。
Lettuce
:高级Redis
客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel
,管道和编码器。
总体来说:优先使用Lettuce
,如果需要分布式锁,分布式集合等分布式的高级特性,添加Redisson
结合使用,因为Redisson
本身对字符串的操作支持很差。
文章第二部分的Redis数据类型参考文章地址:https://juejin.im/post/5b53ee7e5188251aaa2d2e16