阅读 155

redis指南(2): 初露锋芒 | 五大基本类型

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

一、前言

了解了NoSQL的发展历史,我们接下来开始揭开redis的序幕。

Redis以高超的性能,完美的文档、简洁易懂的原码和丰富的客户端库支持在开源中间件领域广受好评。

深入了解Redis的应用和实践,成为如今中高级后端开发的必备技能。

二、入门概述

2.1 它是什么?

Redis 全称 Remote Dictionary Server,中文名为远程字典服务器。redis被好多人形象的比喻成一把瑞士军刀,因为它的功能非常丰富。

查看官网介绍:

Redis 是一种开源(BSD 许可)、内存中数据结构存储,用作数据库、缓存和消息代理。Redis 提供数据结构,例如字符串、散列、列表、集合、带有范围查询的排序集合、位图、超级日志、地理空间索引和流。Redis 内置复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久化,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供高可用性。
复制代码

嗯嗯,看完这个,我们知道既它可以是一个数据库,也可以是缓存和消息代理中间件。它也是我们前面说的NoSQL中的键值存储数据库。

2.2 起源

2008年,意大利一家创业公司Merzia的创始人Salvatore Sanfilippo 发现了MYSQL存在的低性能问题,就决定亲自实现一个数据库,并在2009年开发完成了Redis的最初版本。短短几年后,redis便拥有了一大批的用户群体。

Redis之父网名Antirez 于2020年6月发表退役声明,短短十年左右的时间,说Redis 加速了互联网的进程也不为过,而redis的默认端口 6379 也不是随机选的,由手机键盘的 “MERZ” 的位置来决定,缘于意大利广告女郎在节目上说了一堆愚蠢的话而被他不爽,把她名字做为端口号,称为愚蠢的代名词。

image-20210615173819324

2.3 redis能做什么

redis 具有以下性能:

  • 1、速度快,性能高 :基于内存操作
  • 2、丰富的数据结构: string、list、hash、set、sortedset等不同类型
  • 3、多语言支持
  • 4、持久化:支持数据的持久化,可以把内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • 5、高可用和分布式:主从复制等技术

基于这些特点,redis可以做什么?

1、缓存系统

image-20210615175714744

缓存热点内容,减少数据库压力。

2、计数器

记录贴子的点赞数、评论数和点击数。

image-20210615180042013

3、消息队列

对消息可靠性要求不高的时候,可以基于redis来实现消息队列。

image-20210615180314672

4、排行榜

对热榜帖子的id放进列表,分为总榜单和分类榜单 。

image-20210615180638592

5、分布式锁

常见于高并发环境,通过setnx设置lock在分布式环境下可以使用,也可以加上加锁时间。

当然,实际中可能用不到这么多需求,但是学一门技术的目的就是解决问题,所以,我们需要学习更多的redis高级特性,这样在做一些看似很难完成的任务到时候,说不定redis就可以派上用场。

2.4 redis安装

juejin.cn/post/688628…

三、五大基本类型

这部分内容是 redis 的重中之重,也是入门 redis 的最基础的内容。

reids的命令比较多,下面开始回顾。

3.1 reids-key 基础命令

127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> get name
"xulei"
127.0.0.1:6379> expire name 3
(integer) 1
127.0.0.1:6379> ttl name # 剩余时间
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> exists name
(integer) 0
127.0.0.1:6379> type age
string
复制代码

3.2 string

3.2.1 内部结构

string 是 redis 中 最简单的数据结构,它的内部是一个字符数组。Redis 中所有的数据结构都通过 唯一的key 来获取相应的 value ,而数据类型value 就是区分 这五大基本类型的地方。

string 也是我们最常用的数据结构,如图,我们常见的用途就是存储用户信息,将JSON序列化成String存储,取出时在反序列化。

image-20210615184113228

redis 中的String 的字符串是动态字符串,并非我们传统的字符串数组,即它可以发生动态变换,内部结构类似于Java 的 ArrayList ,在创建初始的时候就会分配一个预分配内存,从而减少内存的频繁分配,这种字符串简称SDS(simple dynamic stirng)。

其内部定义如下:

struct sdshdr {
	// 记录buf数组中已使用字节的数量
	// 等于sds 所保存字符串的长度
	int len;
	// 等于sds所保存字符串的长度
	int free;
	// 字节数组,用于保存字符串
	char buf[];
}
复制代码

image-20210618110323015

该图展示了一个SDS 实例

  • free 属性的值为0,表示这个SDS 没有剩余空间分配
  • len 表示SDS 保存了一个五字节长的字符串
  • buf 属性是一个char 类型的数组,最后一个字节则保存了空字符 ’ \0‘
3.2.2 空间预分配法则和惰性空间释放策略

这两种策略的目的是对未使用空间的一个利用,也就是前面讲的free属性值。

1、空间预分配:

空间预分配用于优化SDS的字符串增长操作,当SDS 的API 对一个SDS 进行修改,并且对SDS进行空间扩展的时候,程序会动态修改已分配空间值和未分配空间值。 类似Java 的ArrayList的扩容。

公式如下:

  • 1、赋值时,当len的长度小于1MB,那么len 属性等于实际的字符串长度,free 的属性值=len值,也就是扩容都是加倍的
  • 2、赋值时,当len 的长度大于等于1MB,那么程序会分配1MB固定的未使用空间。
  • 3、注意:字符串的最大值为512MB

2、惰性空间释放:

惰性空间释放用于优化SDS 的字符串缩短操作,即当字符串个数变少的时候,并不会立即回收多余的空间,而是把空间放在free中。这种避免了缩短字符串所需的内存重分配操作。

当然,SDS也提供相应的API,真正箱释放的时候,也会把这些多余的free释放掉,防止空间占用内存。

3.2.3 常用操作

键值对:

127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> keys *
1) "key1"
2) "age"
127.0.0.1:6379> APPEND key1 hello
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1
(integer) 7
复制代码

加减操作, 可以设置步长

127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> incrby views 10
(integer) 11
127.0.0.1:6379> decrby views 2
(integer) 9
复制代码

字符串范围:

127.0.0.1:6379> set key1 "hello,xulei"
OK
127.0.0.1:6379> getrange key1 0 4
"hello"
127.0.0.1:6379> getrange key1 0 -1 # 获取所有的字符串
"hello,xulei"
复制代码

替换:

127.0.0.1:6379> set key2 abcd
OK
127.0.0.1:6379> get key2
"abcd"
127.0.0.1:6379> setrange key2 1 xx
(integer) 4
127.0.0.1:6379> get key2
"axxd"
复制代码

setex 和setnx:

#setex(set with expire) 设置过期时间
#setnx(set if not exist) 不存在设置

127.0.0.1:6379> setex key3 20 hualin
OK
127.0.0.1:6379> get key3
"hualin"
127.0.0.1:6379> ttl key3
(integer) 5
127.0.0.1:6379> setnx mykey redis # 如果mykey不存在,创建成功
(integer) 1
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
3) "key2"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey 1 # 如果mykey存在,创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
复制代码

批量获取和设置:

# mset 
# mget
# msetnx 是一个原子性的操作,要么一起成功,要么一起失败

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

127.0.0.1:6379> msetnx k1 v1 k4 v4
(integer) 0
127.0.0.1:6379> get k4
(nil)
复制代码

对象:

set user:1 {name:zhangson,age:29}
# 设置一个user:1对象,值为json字符来保存一个对象
127.0.0.1:6379> mset user:2:name xulei user:2:age 26 
OK

########## 
# getset 先get然后set,更新的操作,并返回value
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb
"redis"
127.0.0.1:6379> get db
"mongodb"
复制代码

3.3 List

redis 的list 实际上是一个链表结构,是双向链表,对应Java 语言中的 linkedList , 这种结构带有表头节点指针、表尾节点指针,以及链表长度等信息。因此,它也具有链表的特点,即查询慢, 增删快。

image-20210618114636820

Redis 的列表结构常用来做异步队列使用,一边读取入队,另一边出队。

常用命令:

  • lpush/rpush .... 从左边/右边插入一个或多个值。
  • lpop/rpop 从左边/右边吐出一个值。
  • rpoplpush 从列表右边吐出一个值,插到列表左边
  • lrange
  • lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)
  • lindex 按照索引下标获得元素(从左到右)
  • llen 获得列表长度
  • linsert before 在的后面插入插入值
  • lrem 从左边删除n个value(从左到右)
  • lset将列表key下标为index的值替换成value

常见应用:

127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list tow
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379> lrange list 0 2
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379> lrange list 0 1 #通过区间获取 从尾部依次往前获取
1) "three"
2) "tow"
127.0.0.1:6379> lrange list 1 2 
1) "tow"
2) "one"

127.0.0.1:6379> rpush list zero
(integer) 4
127.0.0.1:6379> lrange list 0 -1 # 插入到头部
1) "three"
2) "tow"
3) "one"
4) "zero"

############# 

127.0.0.1:6379> lpop list # 移除尾部元素
"three"
127.0.0.1:6379> rpop list # 移除头部元素
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"


###################

# 通过下标获取某个值 lindex
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
127.0.0.1:6379> Lindex list 1
"one"
127.0.0.1:6379> Lindex list 0
"tow"
127.0.0.1:6379> Lindex list 2
(nil)

#######################
Llen: 返回列表的长度
127.0.0.1:6379> llen list 
(integer) 3

#####################
# lrem 移除指定的值,移除是从头部开始移除的
取关:uid
127.0.0.1:6379> lrem list 1 1
(integer) 1

127.0.0.1:6379> lrange list 0 -2
1) "2"
2) "2"
3) "1"
4) "2"
5) "2"
6) "3"
127.0.0.1:6379> lrem list 2 2
(integer) 2
127.0.0.1:6379> lrange list 0 -2
1) "1"
2) "2"
3) "2"
4) "3"

###############################
# ltrim 截取list的元素,只剩下剪短后的元素,
# ltrim 1 0 # 是清空整个列表的意思,因为区间长度为负

127.0.0.1:6379> lrange list2 0 -1
1) "hello5"
2) "hello4"
3) "hello3"
4) "hello2"
5) "hello"
127.0.0.1:6379> ltrim list2 2 3
OK
127.0.0.1:6379> lrange list2 0 -1
1) "hello3"
2) "hello2"
127.0.0.1:6379> 

#################
rpoplpush # 移除列表的最后一个元素

127.0.0.1:6379> rpush mylist hello1
(integer) 1
127.0.0.1:6379> rpush mylist hello2
(integer) 2
127.0.0.1:6379> rpush mylist hello3
(integer) 3
127.0.0.1:6379> rpush mylist hello4
(integer) 4
127.0.0.1:6379> rpush mylist hello5
(integer) 5
127.0.0.1:6379> rpoplpush mylist othorlist
"hello5"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> lrange othorlist 0 -1
1) "hello5"

#################
# lset 将列表中指定的下标的值替换为另外一个值 更新操作

127.0.0.1:6379> lset list 0 item # 判断这个列表是否存在,
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lset list 0 item
OK
127.0.0.1:6379> lrange list 0 0
1) "item"

####################
# linsert 在before和after插入 具体的值

127.0.0.1:6379> lrange list 0 -1
1) "intem3"
2) "intem2"
3) "item"
127.0.0.1:6379> linsert list before "item" nimabi 
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "intem3"
2) "intem2"
3) "nimabi"
4) "item"
127.0.0.1:6379> linsert list after item hahahhahah
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "intem3"
2) "intem2"
3) "nimabi"
4) "item"
5) "hahahhahah"
复制代码

3.4 set(集合)

redis 的 set 集合相当于 Java 语言里面的 HashSet ,它的内部的键值对是无序的、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value 都是一个值 NULL ,当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。

set 中的值是不允许重复的,无序的。可以用来存储在某个活动中 中奖的用户ID,可以去重作用,保证一个用户不会中奖两次。

常用命令:

  • sadd key value1 value2 :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略。
  • smembers key :取出该集合的所有值。
  • sismember key value :判断集合key是否含有该value值,有1,无返回0
  • scard key value :返回该集合的元素个数
  • srem key value :删除集合中的某个元素
  • spop key :随机从该集合中吐出一个值
  • srandmember key n :随机中该集合中取出n个值,不会从集合中删除
  • smove source destination value :把集合中的一个值从一个集合移动到另一个集合
  • sinter key1 key2 :返回两个集合的交集元素
  • sunion key1 key2 :返回两个集合的并集元素
  • sdiff key1 key2 :返回两个集合的差集元素(key1 中的,不包含key2中的)

命令应用:

##################################
# sadd 添加元素
# smembers :所有的set集合
# sismember :判断是否有该元素
# scard :获取当前的元素

127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset xiaolei
(integer) 1
127.0.0.1:6379> sadd myset xulei love hualin
(integer) 3
127.0.0.1:6379> smembers myset
1) "xulei"
2) "hello"
3) "love"
4) "xiaolei"
5) "hualin"
127.0.0.1:6379> sismember myset hello
(integer) 1
127.0.0.1:6379> scard myset
(integer) 5
########################################
# srem : 移除set集合元素
# SRANDMEMBER:随机抽选出一个元素

127.0.0.1:6379> srem myset hello
(integer) 1
127.0.0.1:6379> SRANDMEMBER myset 
"xulei"
127.0.0.1:6379> SRANDMEMBER myset 
"hualin"
127.0.0.1:6379> SRANDMEMBER myset 
"love"

########################################
# spop :随机删除key

127.0.0.1:6379> spop myset
"hualin"
127.0.0.1:6379> SMEMBERS myset
1) "love"
2) "xulei"
3) "xiaolei"
127.0.0.1:6379> spop myset
"love"

########################################
# 将一个指定的值,移动到另外一个集合中

127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> sadd myset xulei
(integer) 1
127.0.0.1:6379> sadd myset2 hualin
(integer) 1
127.0.0.1:6379> sadd myset2 xiangni
(integer) 1
127.0.0.1:6379> smove myset myset2 xulei
(integer) 1
127.0.0.1:6379> SMEMBERS myset2
1) "xulei"
2) "xiangni"
3) "hualin"

########################################
应用:微博,b站 把所有关注的人放在一个set集合中,将它的粉丝也放在一个集合中。
数字集合类:
    差集:SDIFF
    交集:SINTER
    并集:SUNION
127.0.0.1:6379> sadd key1 a
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
127.0.0.1:6379> SDIFF key1 key2
1) "b"
复制代码

3.5 zset (有序列表)

zset 在set 的基础上增加了一个值,分组。set k1 v1 zset k1 score1 v1

它类似于SortedSet 和HashMap 的结合体,一方面它是一个set,保证了value 的唯一性,另一方面,它可以给每个value 赋予一个 score,代表这个value 的排序权重,它的内部实现用的是跳转列表的数据结构。

image-20210618132322062

应用场景:

  • zset 可以用来存储粉丝列表,value 值是粉丝的用户Id ,score 是关注事件,可以按照关注时间排序。
  • zset 还可以用来存储学生的成绩,value 是学生的Id ,score 是考试成绩。

实操:

##########################
# ZRANGEBYSCORE 排序 min max
# ZREVRANGE :从大到小排序
# zrange : 从小到大排序

127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"

127.0.0.1:6379> zadd salary 2500 xiaohon
(integer) 1
127.0.0.1:6379> zadd salary 9000 zhansan
(integer) 1
127.0.0.1:6379> zadd salary 200 xulei
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf
1) "xulei"
2) "xiaohon"
3) "zhansan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores
1) "xulei"
2) "200"
3) "xiaohon"
4) "2500"
5) "zhansan"
6) "9000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores
1) "xulei"
2) "200"
3) "xiaohon"
4) "2500"

127.0.0.1:6379> ZREVRANGE salary 0 -1
1) "zhansan"
2) "xiaohon"

##########################
# rem:移除rem中的元素:移除有序集合中的元素
# zcard:查看里面的元素个树
# zcount:计算区间内的个树

127.0.0.1:6379> zrange salary 0 -1
1) "xulei"
2) "xiaohon"
3) "zhansan"
127.0.0.1:6379> zrem salary xulei
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xiaohon"
2) "zhansan"
127.0.0.1:6379> zcard salary
(integer) 2

127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 xulei
(integer) 2
127.0.0.1:6379> zcount myset 1 3
(integer) 3

复制代码

3.6 Hash (字典)

相当于 Java 语言 里面的HashMap ,也就是数组+链表组合。

hash 结构存储多个key-value ,可以对单独的key做获取,本质上和string 类型没有太大的区别。

hash 对比 string

  • 1、如果存储的都是比较结构化的数据,比如用户数据缓存,特别是如果一个数据中如果字段比较多,但是每次只需要使用其中的一个或者少数的几个,使用hash 是一个好的选择,因为它提供了hget 和hmget,而无需取出所有数据在代码中处理。

  • 2、如果数据差异很大,需要把所有数据读取出来再处理,则用string 来完成

  • 3、hash缺点:存储消耗高于单个字符串。

常用命令:

  • hset key field value :给 key 集合中的 field 键赋值 value
  • hget key field value : 从key集合filed 属性中取值
  • hmset key field1 value field2 value :批量设置值
  • hmget key field1 value field2 value :批量获取值
  • hkeys key :获取所有field
  • hvals key :获取所有的value

基本命令实操:

#################################
# hset:存值
# hget: 取值
# hmset: 存多个值
# hmget: 取多个值
# hgetall: 获取所有的key value
# hdel: 删除指定的key
127.0.0.1:6379> hset myhash field1 xulei
(integer) 1
127.0.0.1:6379> hget myhash field1
"xulei"
127.0.0.1:6379> hset myhash field1 xulei
(integer) 1
127.0.0.1:6379> hget myhash field1
"xulei"
127.0.0.1:6379> hmset myhash field2 2 field3 3
OK
127.0.0.1:6379> hmget myhash field1 field2 field3
1) "xulei"
2) "2"
3) "3"
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "xulei"
3) "field2"
4) "2"
5) "field3"
6) "3"
127.0.0.1:6379> hdel myhash field2
(integer) 1

#################################
# hlen:查看hash表的字段数量
# hexists: 判断指定hash是否存在
# hkeys: 查看当前所有的key
# hvals:只获得所有的value

127.0.0.1:6379> hlen myhash
(integer) 2
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "xulei"
3) "field3"
4) "3"
127.0.0.1:6379> hexists myhash field1
(integer) 1
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field3"
127.0.0.1:6379> hvals myhash
1) "xulei"
2) "3"

################################
# hincryby :指定增量
# hsetnx : 如果存在则不能设置

127.0.0.1:6379> hset myhash field4 4
(integer) 1
127.0.0.1:6379> hincrby myhash field4 2
(integer) 6
127.0.0.1:6379> hincrby myhash field4 -4
(integer) 2
127.0.0.1:6379> 
复制代码

四、小结

这篇文章介绍了redis的功能介绍和五大基本类型的使用,我们也正式开启了nosql 的大门,后续更多高级的redis指南,将在后期奉上,喜欢的点赞,原创不易,你的点赞将是我创作的最好动力。

本文参考:

  • 《Redis 深度历险》
  • 《Redis 设计与实现》
文章分类
后端
文章标签