【redis1】基本指令,五大数据类型,存储优化,使用场景】

初学者建议先看redis导论[https://blog.csdn.net/m0_58718491/article/details/161198350?spm=1001.2014.3001.5502](https://blog.csdn.net/m0_58718491/article/details/161198350?spm=1001.2014.3001.5502 "https://blog.csdn.net/m0_58718491/article/details/161198350?spm=1001.2014.3001.5502")提及的分布式系统演进的变化背景,再看本篇

一、redies简介

要点

键值对:Redis(Remote Dictionary Server)提供了基于键值对(key-value)的存储系统,key是String,value是五大数据类型

性能

  • 快!数据都存放在内存中,所以读写性能非常出色,不涉及磁盘 I/O(持久化除外);

  • 核心功能操作简单,操作内存不怎么消耗cpu;网络角度使用了io多路复用(epoll;

  • 单线程减少线程竞争开销,后续引入了多线程但只是在io方面(多线程适用适合 I/O 密集型任务,一个线程发起 I/O 请求时(比如等 MySQL 返回数据),这个线程就会进入阻塞,CPU 是完全闲着的可以切换给另一个线程去执行别的任务)

IO 多路复用 :允许单个线程同时监控多个网络连接(套接字)的技术;Redis 利用了网络通信中'绝大多数 Socket 都在潜水'的客观规律**。** 它抛弃了高内耗的'一连接一线程'模式,采用了基于 epoll 的 I/O 多路复用技术

单线程:redis只用一个线程,处理所有的命令请求,多个线程是在处理网络IO,比如多个redies客户端申请修改数据(I/O多线程),但是在redies服务端还要排队,一个一个取处理请求(单线程)

原子:Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行

可编程

  • 用户可以编写一段脚本或程序,并将其发送到 Redis 服务器端执行的能力,Redis 最主要的可编程性是通过集成Lua 脚本引擎 实现;

  • 没有可编程性前:想实现"如果余额足够则扣款"的逻辑,从 Redis 读余额--在服务器里判断--如足够写回新余额;

  • 有了可编程性:你直接写一段 Lua 逻辑交给 Redis。Redis 内部会帮你锁定数据、判断逻辑、执行修改,一气呵成

可扩展

  • 开发者可以使用 C, C++, 或 Rust 等底层语言来编写扩展程序,在编译后本质上是一个动态链接库,在 Windows 上表现为 .dll 文件,供 exe 程序调用;

  • 可以通过扩展让 Redis 支持全新的数据类型(如复杂的地理信息、全文搜索索引);

  • 可以为 Redis 增加自定义命令,这些命令执行起来就像原生命令(如 SETGET)一样高效

持久化:它会利用快照和日志的形式将内存的数据持久化到硬盘上,防止数据丢失。

支持集群:Redis 作为中间件,支持集群模式;

  • 主从集群模式:主写从读;

  • 水平扩展:引入多个主机并部署多个 Redis 节点,让每个节点只存储数据的一部分;

  • 基于哈希的分片:集群规模增长时,系统支持自动重新分区

高可用

  • ①冗余与备份

    Redis 原生支持主从结构。从节点本质上就是主节点的一个完整数据备份

    通过冗余或备份来消除单点故障

  • ②自动故障转移

    系统自动监控主节点状态;一旦主挂掉,会自动将一个从节点提升为新的主节点,接管服务

场景

  • 常用于实时性数据库(如广告搜索,存广告相关全量数据);

  • 热点数据缓存;会话缓存(缓存session);

  • 消息队列(网络版本的生产者消费者模型,解耦合,削峰填谷);业界也有很多知名的消息队列,如RabbitMQ, Kafka, RocketMQ

  • 计数器、排行榜、社交网络、分布式锁等应用场景。

session缓存问题:早期的 Session 数据直接存储在对应的应用服务器内存;当引入负载均衡器(Load Balancer)后,用户的第一次请求可能打在 A 机器(并记录了 Session),但第二次请求可能被分发到 B 机器。由于 B 机器内存中没有该用户的 Session,会导致用户被迫重新登录

  • 会话粘滞:让负载均衡器不再简单地进行轮询到哪个服务器上,而是通过 userIdIP 进行哈希计算,确保同一个用户的请求始终打到同一台机器上

  • 分布式会话:将会话数据从应用服务器中"单独拎出来",放到一组独立的机器上存储,通常使用 Redis

  • redis客户端和服务器可以在同一个主机上,也可以在不同主机上;

  • 同一时刻一个服务器可以服务多个客户端;服务器是(本体)负责存储和管理数据;

  • 客户端可以通过cli、图形化界面、通过api自己开发客户端操作

  • 自己使用一般只有一台机器,此时客户端和服务器就是在同一个机器

  • 使用hash map是直接操作内存;使用redis是先通过网络!!再操作内存

二、场景举例-String的使用

缓存(Cache)功能

比如通过用户id获取用户信息-----首先从 Redis获取用户信息,我们假设用户信息保存在"user:info:<uid>"对应的键中-----如果没有从Redis中得到用户信息,及缓存miss,则进一步从MySQL中获取对应的信息,随后写入缓存设置过期时间并返回-----通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次MySQL查询,极大地提升了查询效率,也降低了MySQL的访问数。

  • 应用服务器访问数据的时候,先查Redis

  • 如果Redis上数据存在了,就直接从Redis 取数据交给应用服务器,不继续访问数据库了

  • 如果Redis上数据不存在,再读取MySQL,把读到的结果, 返回给应用服务器同时,把这个数据也写入到Redis中

  • Redis上数据越写越多怎么办?1)把数据写给redis的同时,给这个key 设置一个过期时间;2)内存不足时,使用淘汰策略

视频播放量计数

异步写入:写入统计数据仓库(可能是mysql,也可能是hdfs..)的步骤,不是说,来一个播放请求,这里就必须立即马上写一个数据;

实际中要开发一个成熟、稳定的计数系统,要面临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。

共享会话(Session)

手机验证码

由于验证码具备"高并发、高吞吐、时效性极强(通常5分钟失效)、用完即毁"的特点,如果直接塞进 MySQL 这种传统关系型数据库,不仅会把数据库的 I/O 瞬间拖垮,还会留下大量无用的历史垃圾数据。【优势:自带 TTL 自动过期机制;内存级抗高并发】

核心业务流水线

生成并发送验证码(写入 Redis)

  • 操作 :用户点击"获取验证码",后端生成一个 6 位的随机数字(如 573921)。

  • 存储结构 :通常使用 Redis 的 String(字符串) 结构。

    • Key 设计lock:sms:code:手机号 (例如:lock:sms:code:138xxxx1234

    • Value573921

  • 关键指令 :使用 SETEX 指令,在写入的同时强制设置过期时间(比如 5 分钟,即 300 秒):

  • 后端将验证码对接给第三方短信网关(如阿里云、腾讯云),发送到用户手机

用户登录验证(读取并比对 Redis)

  • 操作 :用户收到短信,在输入框填入 573921 并提交登录。

  • 校验逻辑

    1. 后端接收到手机号和用户输入的验证码。

    2. 去 Redis 中执行 GET lock:sms:code:138xxxx1234

    3. 比对结果

      • 如果返回为空(nil),说明验证码已过期或从未发送,拒绝登录。

      • 如果返回的值和用户输入的不一致,提示"验证码错误"。

      • 如果值完全一致,则通过验证。

  • 安全销毁 :一旦验证通过,通常会立刻执行 DEL 指令把这个 Key 删掉,防止同一个验证码在有效期内被二次利用。

二、五大数据类型

它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

redis自身的这些键值对,是通过哈希表的方式来组织的,redis具体的某个值,又可以是一些数据结构

五大数据类型及应用场景

  • String:验证码、计数器、分布式 Session。

  • Hash:存储对象信息(如用户信息、购物车)。

  • List:消息队列(简易版)、时间线--只能存储字符串(Strings)类型的数据。

  • Set:共同好友、抽奖、点赞。

  • ZSet:排行榜、带权重的任务队列。

Redis中的字符串,直接就是按照二进制数据的方式存储的,遇到乱码概率小(不会做任何的编码转换,存啥取啥)不仅仅可以存储文本数据,JSON、xml二进制数据(视频,音频...

sql vs redies

其他数据类型

(特殊场景下使用)

Streams(流):一个只追加(Append-only)的日志型数据流

Geospatial(GEO/地理位置):基于ZSet 实现的地理位置数据结构。它利用 GeoHash 算法

HyperLogLog:一种用于 基数统计的概率性算法数据结构

Bitmaps(位图):底层是普通的 Strings ,但它允许你按位(Bit,即 0 和 1)进行操作

Bitfields(位域):在 Bitmaps 基础上的进一步增强,允许开发者操作任意位长度的整数

Redis存储优化

同一个数据类型,背后可能的编码实现方式是不同的,它会根据特定场景进行优化,来达到节省时间/节省空间效果

1. String(字符串)的底层优化

String 是最基础的结构,但在底层为了榨干内存,分成了三种内部编码:

  • int(整数编码)

    • 优化场景 :当字符串的值是一个纯整数 (如计数器 1002026)时。

    • 原理 :Redis 会直接将其转换为 long 类型的整数来保存,不需要额外的内存分配(利用**指针的宽度刚好等于整数的宽度,**直接把数据硬塞进了指针变量的身体里),非常适合做计数、限流等高频操作。

  • embstr(嵌入式字符串)

    • 优化场景 :针对短字符串(小于等于 44 字节)的特殊优化。

    • 原理 :它将 Redis 对象头(RedisObject)和实际的字符串结构体(SDS)分配在一块连续的内存空间中。由于只需要一次内存分配和释放,且对 CPU 缓存极为友好,性能极高。

  • raw(普通字符串)

    • 优化场景 :当字符串长度超过 44 字节时。

    • 原理:由于空间太大,Redis 会分别调用两次内存分配,将对象头和字符串结构体存放在不同的内存碎片中。


2. Hash(哈希)的底层优化

  • ziplist(压缩列表)

    • 优化场景:当 Hash 中的元素个数较少,且每个元素的键值对长度都很短时。

    • 原理 :它在底层是一块连续的内存空间,像数组一样紧凑排列。没有指针的额外开销,极大节省了内存空间(因为元素少,遍历查询不费时。

  • hashtable(字典/散列表)

    • 优化场景:当元素数量变多或某些 field 的值太大时。

    • 原理 :自动升级为标准链式哈希表(类似 Java 的 HashMap),确保在数据量巨大时,查找和修改的时间复杂度依然保持在 O(1)


3. List(列表)的底层优化

  • linkedlist(双向链表)与 ziplist(压缩列表)

    • 早期 Redis 也是小数据量用 ziplist 节省内存,大数据量升级为 linkedlist(双向链表)。
  • 🌟 现代优化:quicklist(快速列表)

    • 在现代 Redis 版本中,List 的底层基本被统一优化为了 quicklist 。它完美结合了前两者的优点:将多个 ziplist(相当于元素) 用双向链表连接起来。既避免了纯链表频繁开辟指针带来的内存浪费,又避免纯压缩列表扩容时的内存复制成本。

4. Set(集合)的底层优化

  • intset(整数集合)

    • 优化场景 :当集合中的所有元素全是整数,且数量较少时。

    • 原理:底层是一个完全连续的、有序的整数数组。查找时采用二分查找。由于没有哈希冲突指针的开销,极度省内存。

  • hashtable(散列表)

    • 优化场景 :一旦集合中出现了非整数元素 (如字符串 apple),或者元素数量超过阈值。

    • 原理 :直接转为 hashtable,其中所有的 Value 都指向 NULL(类似于 Java 中 HashSet 底层依赖 HashMap)。


5. ZSet(有序集合)的底层优化

  • ziplist(压缩列表)

    • 优化场景:当有序集合的元素数量较少,且成员长度较短时。

    • 原理 :在一块连续内存里,紧挨着存放 memberscore,通过空间紧凑性来换取低内存消耗。

  • skiplist(跳跃表)

    • 优化场景:当数据量变大时。

    • 原理 :升级为跳跃表 + 字典的复合结构。字典用来保证根据 member 查找 score 的复杂度为 O(1),而跳跃表,每个节点上有多个指针域,巧妙的搭配这些指针域的指向,就可以做到,从跳表上查询元素的时间复杂度是O(logN)

三、redies全局命令

操作不同的数据结构就会有不同的命令;

全局命令,就是能够搭配任意一个数据结构来使用的命令

redis是单线程的程序,主要的任务(处理每个命令的任务,扫描过期key....)

object encoding key

查看key 对应的value的实际编码方式

sql 复制代码
set key1 111
--返回ok
get key1
--返回"111"
type key1
--string
OBJECT encoding key1
--"int"

set/get

对于GET只支持,字符串类型的value;如果value是其他类型,使用GET获取就会出错

sql 复制代码
set key1 value1
--返回ok
get key1
--返回"valuel"
get key100
--(nil)--不存在

keys

用来查询当前服务器上匹配的key;返回所有满足样式(pattern)的key。时间复杂O(N)

在生产环境上禁止使用keys命令,尤其是keys *时间非常长,就使redis 服务器被阻塞了,无法给其他客户端提供服务

pattern:包含特殊符号的字符串,存在的意义,是去描述字符串长啥样的

sql 复制代码
set hello 1
--OK
set hallo 1
--OK
set heeeeeeeello
--OK
keys h?llo
--"hello"
--"hallo"
keys h*llo
--"hello"
--"hallo"
--"heeeeeeeello"

exists

exists 判定key是否存在,时间复杂度O(1),redis组织这些key就是按照哈希表的方式来组织的

sql 复制代码
EXISTS hello
--(integer) 1
EXISTS hello hallo
--(integer) 2
--分开写exists hello;exists hallo需要两次网络请求
--性能稍低,接收方收到一个数据,这个数据就要从物理层,到应用层层层分用

del (delete)

删除指定的key,时间复杂度O(1)

sql 复制代码
del hello hallo 
--(integer) 2
del aaa
--(integer) 0

expire

作用是给指定的 key 设置过期时间,key存活时间超出这个指定的值,就会被自动删除,时间复杂度O(1)

如手机验证码;限时优惠券;分布式锁的过期时间

EXPIRE key seconds

pexpire key 毫秒

sql 复制代码
set he 111
--OK
expire he 10
--(integer) 1--表示设置成功
get he
--"111"
get he
--(nil)--十秒后

ttl

获取指定key的过期时间,秒级。语法:TTL key;间复杂度O(1)

返回值剩余过期时间。-1表示没有关联过期时间,-2表示key不存在

sql 复制代码
ttl hello
--(integer) -2--表示key不存在

过期策略

一个redis中可能同时存在很多很多key,redis 服务器咋知道哪些key已经过期要被删除,哪些key还没过期?

  • 定期删除:每次抽取一部分,进行验证过期时间,保证这个抽取检查的过程,足够快

  • 惰性删除:访问key时就会让redis服务器触发删除key的操作,同时再返回一个nil

  • 淘汰策略:过期的key被残留了,没有及时删除掉,使用内存淘汰策略

定时器

  • 基于优先级队列/堆:可以通过"过期时间越早,就是优先级越高";此时只要分配一个线程,去检查队首元素,如果队首元素还没过期,后续元素一定没过期(不用检查太频繁,防止cpu忙等空转)此时做法就是可以根据当前时刻和队首元素的过期时间,设置一个等待;---需要多个独立线程专门维护定时器队列【redies不采用,因为是单线程】

  • 基于时间轮:把时间划分成很多小段,每个小段是一个链表,每个链表表示一个要执行的任务;指针(是一个单独线程)就会每隔固定的间隔每次走到一个格子,就会把这个格子上链表的任务尝试执行一下【redies没采用】

  • redies采用的是上面的定期、惰性、淘汰方案

type

查看key对应值的数据类型;none表示key不存在;间复杂度O(1)

list,string,set,zset有序集合,hash哈希表,stream(redies作为消息队列时候使用的

sql 复制代码
type key
--none
set key1 111
--OK
type key1
--string
lpush key2 111 222 333
--(integer) 3---lpush是头插创建一个list,sadd是创建set,hset是创建哈希表
type key2
--list

FLUSHALL

清空所有数据

小结

keys:用来查看匹配规则的key

exists:用来判定指定key是否存在

del:删除指定的key

expire:给key设置过期时间.

ttl:查询key的过期时间

type:查询key对应的value的类型

五、redies数据命令

1.对于String

SET key value

SET key value expiration EX seconds \| PX milliseconds NX\|XX

  • 过期时间参数(二选一)

    • EX seconds:设置键的过期时间,单位为

    • PX milliseconds:设置键的过期时间,单位为毫秒

  • 存在性条件参数(二选一)

    • NX (Not Exists):只有当 Key 不存在时才执行设置(常用于实现分布式锁)。

    • XX只有当 Key 已经存在时才执行设置,用于更新值。

执行 set key value ex 10 它的实际效果完全等同于连续执行以下两条命令:

  1. set key value

  2. expire key 10

MSET/MGET

一次性设置多个key的值:MSET key value key value ...

时间复杂度O(N);N是当前命令key的数量约等于O(1)

sql 复制代码
mset key1 111 key2 222 key3 3333
--OK
mget key1 key2 key3
--1)"111"
--2)"222"
--3)"3333"

SETNX/SETEX/PSETEX

SETNX (Set if Not Exists):如果key不存在,则设置value,并返回 1

SETEX(Set with Expiration):设置值并指定生存时间(秒),原子性操作

PSETEX(Precise Set with Expiration):设置值并指定生存时间(毫秒)

sql 复制代码
setnx key1 111
--(integer) 1
get key1
--"111"
setnx key1 222
--(integer) 0
setex key2 10 222
--OK--设置key2的值为222有效期为10s
psetex key3 5000 333
--OK--设置key3的值为333有效期为5000ms
pttl key3
--(integer)1633--还剩1633ms
pttl key3
--(integer) -2--已过期,key不存在

incr/incrby/decr/decrby/incrbyfloat

【可以用于实现计数器,如统计视频播放次数,时间复杂度都是O(1),由于redis处理命令的时候,是单线程模型.多个客户端同时针对同一个key 进行incr操作,不会引起"线程安全"问题】

incr key n:针对value +1,key对应的value必须得是整数(8byte--java_long),返回+1后的值

sql 复制代码
set key 10
--OK
incr key
--(integer) 11
get key
--"11"
incr key66
----(integer) 1--key66不存在,初始值默认从0开始+1

incrby key n:针对value+n,value必须得是整数,可以是负数,相当于减法

sql 复制代码
incrby key 10
--(integer) 21

decr key n:针对value -1

sql 复制代码
decr key68
----(integer) -1--key68不存在,初始值默认从0开始-1

decrby key n:针对value -n

incrbyfloat key n:把key对应的value进行+-n运算,运算的操作数可以是浮点数

append/getrange/setrange/strlen

APPEND KEY VALUE:如果key 已经存在并且是一个string,命令会将value 追加到原有string的后边。如果key 不存在,则效果等同于SET命令;

时间复杂度:O(1);

返回值:追加完成之后string的长度,长度的单位是字节

sql 复制代码
set key hello
--OK
APPEND key world
--(integer) 10
get key
--"helloworld"
append key2 你好
--(integer) 6
--redis不认识字符,只认识字节
--xshell终端,默认的字符编码是utf8.
--在终端输入汉字之后,也就是按utf8编码的
--一个汉字在utf8字符集中,通常是3个字节
get key2 
--"\xe4\xbd\xa0\xe5\xa5\xbd"--以16进制utf8编码返回

GETRANGE key start end:获取字符串的子串(java为substring),由start和end确定(左闭右闭),可以使用负数表示倒数。-1代表倒数第一个字符,超过范围的偏移量会根据string的长度调整成正确的值。

时间复杂度:O(N)

返回值:string类型的子串

若字符串中保存的是汉字,进行切分是强行切出了中间的几个字节,结果在utf8码表上不知道能查出啥了,不一定是完整的汉字,Java中String帮我们把汉字的编码转换都处理好了

sql 复制代码
set key helloworld
--OK
getrange key 0 -1
--helloworld
getrange key 1 -2
--elloworl

SETRANGE key offset value:偏移量offset(表示从第几个字节,开始进行替换),返回值是 替换之后 新的字符串的长度

sql 复制代码
set key helloworld
--OK
setrange key 5 aaaaaaaa
--(integer) 10
get key
--"helloaaaaaaaa"
setrange key2 1 aaa
--get key2
--"\x00aaa"--
--对于不存在的key2,凭空生成了一个字节,
--这个字节里的内容就是0x00,aaa就被追加到0x00的后面了

STRLEN key:获取到字符串的长度,单位是字节(Java中,字符串的长度则是以字符为单位的,1char=2byte,是unicode编码,2字节表示一个汉字)

MySQL的varchar(N)此处N的单位就是字符,可用存N个汉字,这样的一个字符,可能是多个字节

时间复杂度:O(1)

返回值:string的长度。或者当key不存在时,返回0

sql 复制代码
set key 你好
--OK
strlen key
--(integer) 6--三字节一个汉字(redis是utf8

--raw

CTRL+D退出Redies客户端(CTRL+S谨慎按,在xshell中的作用是"冻结当前画面",效果像卡死,CTRL+Q解除冻结

在启动redis客户端的时候,加上一个--raw这样的选项,就可以使redis客户端能够自动的把二进制数据尝试翻译

sql 复制代码
redis-cli --raw

get key2
--你好

小结

命令 执行效果 时间复杂度 核心应用场景
set key value 设置指定 key 的值为 value O(1) 基础数据缓存、Token 存储
get key 获取指定 key 的值 O(1) 获取缓存数据
del key [key ...] 删除一个或多个指定的 key O(k) (k为删除的key个数) 清理过期缓存、注销登录
mset key value [key value ...] 批量设置多个 keyvalue O(k) (k为设置的key个数) 批量初始化数据,减少网络往返
mget key [key ...] 批量获取多个 key 的值,和并多次操作减少通信成本 O(k) (k为获取的key个数) 批量查询(如:同时查多个用户的基本信息)
incr key 将指定 key 的整数值 +1 O(1) 文章点赞数、视频播放量统计
decr key 将指定 key 的整数值 -1 O(1) 保证不超卖的秒杀库存扣减(简单场景)
incrby key n 将指定 key 的整数值 +n O(1) 游戏积分累加
decrby key n 将指定 key 的整数值 -n O(1) 扣除账户余额/积分
incrbyfloat key n 将指定 key 的浮点数值 +n O(1) 涉及小数的业务统计(如:资金变动)

2.对于Hash

Redis自身的键值对就是通过哈希的方式来组织的;把key这一层组织完成之后,到了value这一层;value的其中一种类型还可以再是哈希 field-value

结构:【key】-【value field-value

HSET

设置hash中指定的字段(field)的值(value)

时间复杂度:O(1)

返回值:添加的字段的个数。

sql 复制代码
--一次命令,设置多组,减少io开销
HSET key field value [field value ...]   

hset key f2 222 f3 333 f4 444
--(integer) 3

HGET

获取hash中指定字段的值

sql 复制代码
HGET key field

hget key f1
--(nil)
hget key f2
--"222"
--⭐进行两次hash计算,第一次计算key名字,第二次计算f2名字

HEXISTS

判断hash中是否有指定的字段

时间复杂度:O(1)

返回值:1表示存在,0表示不存在

sql 复制代码
HEXISTS key f2
--(integer) 1
HEXISTS key2 f2
--(integer) 0
HEXISTS key f100
--(integer) 0

HDEL

删除hash中的指定字段;del 删除的是key;hdel 删除的是field

时间复杂度:O(1)

返回值:成功删除的字段的个数。

sql 复制代码
--一次命令,设置多组,减少网络传输开销
HDEL key field value [field value ...]   

hdel key f2 f3 
--(integer) 2
hdel key2 f4
--(integer) 0
del key
--(integer) --把整个key存的hash value都删了

HKEYS

获取hash中的所有字段field---有一定风险,类似keys *

时间复杂度:O(N)

返回值:字段列表

---先根据key找到对应的hash,O(1)

---然后再遍历hash, O(N),N为key中hash对个数

sql 复制代码
HKEYS key

hkeys key
--"f4"

HVALS

获取hash中的所有字段field的value

这个操作的时间复杂度,也是O(N),N是哈希的元素个数

如果哈希非常大,这个操作就可能导致redis服务器被阻塞住

sql 复制代码
HVALS key

hvals key
--"444"

HGETALL

获取hash中的所有字段field+对应value

sql 复制代码
hgetall key
--"f4"
--"444"

HMGET

可以一次查询多个field;HGET一次只能查一个field

HMSET和HSET都可以设置多个

sql 复制代码
hmget key f1 f2 f3

HLEN

获取指定kay对应hash中的所有字段的个数

时间复杂度:O(1)-----有一个遍量存储这个个数,不用遍历

返回值:字段个数

sql 复制代码
HSET key2 f1 111 f2 222 f3 333
--(integer) 3
hlen key2
--(integer) 3

HSETNX

类似于setnx,不存在的时候,才能设置成功.如果存在,则失败.

sql 复制代码
HSETNx key field value
hsetnx key f5 555
--(integer) 1
 hsetnx key f5 666
--(integer) 0

HINCRBY

将hash中字段对应的数值添加指定的值

时间复杂度:O(1)

返回值:该字段变化之后的值

sql 复制代码
HINCRBY key field increment

hincrby key f5 -55
--(integer) 500

hash这里的value,也可以当做数字来处理,hincrby就可以加减(加负数)整数

hincrbyfloat就可以加减小数

小结

**hkeys, hvals, hgetall:**存在一定风险,hash的元素个数太多,耗时较长,从而阻塞redis

**hscan:**遍历redis的hash,但属于"渐进式遍历";敲一次命令,遍历一小部分(时间可控)

**ConcurrentHashMap:**哈希表在扩容的时候,也是按照化整为零的方式进行

命令 执行效果 时间复杂度 核心解析与注意事项
hset key field value [field value ...] 设置单个或多个 field-value 对 O(1)或 O(K) K为设置的字段对数。若 field 已存在则直接覆盖。
hget key field 获取指定 field 的值 O(1) 若 key 或 field 不存在,返回 nil
hdel key field [field ...] 删除一个或多个 field O(K) K 为要删除的 field 个数。返回成功删除的个数。
hlen key 计算指定 key 中 field 的个数 O(1) 内部有计数器直接读取,无需全表扫描。
hgetall key 获取该 key 下所有的 field-value O(N) N 为该 Hash 的总元素量。大 key 慎用,容易阻塞单线程。
hmget key field [field ...] 批量获取指定 field 的值 O(K) k 为请求的 field 个数。(修正了原图漏掉 key 的问题)
hmset key field value [field value ...] 批量设置多个 field-value 对 O(K) 旧版常用,新版直接用 hset 即可。
hexists key field 判断指定 field 是否存在 O(1) 存在返回 1,不存在或 key 不存在返回 0
hkeys key 获取该 key 下所有的 field 名 O(N) N 为 field 总数。相当于 Java 中 Map 的 keySet()
hvals key 获取该 key 下所有的 value 值 O(N) N 为 field 总数。相当于 Java 中 Map 的 values()
hsetnx key field value 只有在 field 不存在时才设置成功 O(1) 原子性操作,常用于分布式锁细粒度键值的安全初始化。
hincrby key field n 将对应 field 的值加上整数 n O(1) n 可以为负数。要求原本的 value 必须是能解析的整数。
hincrbyfloat key field n 将对应 field 的值+-上浮点数 n O(1) 针对双精度浮点数的数学自增计数器。
hstrlen key field 计算指定 field 对应 value 的字符串长度 O(1) 如果 field 不存在,则返回 0

哈希的内部编码

ziplist (压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)同时所有值都小于hash-max-ziplist-value 配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面hashtable更加优秀。

hashtable (哈希表):当哈希类型无法满足ziplist的条件时,Redis 会使用hashtable 作为哈希的内部实现,因为ziplist 的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

控制哈希在ziplist和hashtable两种内部编码的转换,可能会造成内存的较大消耗。

sql 复制代码
 hmset hashkey f1 v1 f2 v2
--OK
object encoding hashkey
--"ziplist"

string(jison): 如果使用 string(jison)的格式来表示Userlnfo,万一只想获取其中的某个field,或者修改某个field;就需要把整个json都读出来,解析成对象,操作field,再重写转成json字符串,再写回去

hash:如果使用hash的方式来表示Userlnfo就可以使用field表示对象的每个属性(数据表的每个列)此时就可以非常方便的修改/获取任何一个属性的值了;使用hash的方式,确实读写field更直观高效,但是付出的是空间的代价

原生字符串类型:使用字符串类型,每个属性一个键。这种方式,相当于把同一个数据的各个属性,给分散开表示了,低内聚~~

set user:1:name James

set user:1:age23

3.关于List

列表(List)相当于数组或者顺序表;列表中的元素是有序的

如果把元素位置颠倒,顺序调换;此时得到的新的List和之前的List是不等价的

列表中的元素是允许重复的,像hash这样的类型,field是不能重复的

List,头和尾都能高效的插入删除元素,就可以把这个List当做一个栈/队列来使用了

【Redis有一个典型的应用场景,就是作为消息队列.最早的时候,就是通过List类型;Redis又提供了一个steam类型,但用起来较复杂】

LPUSH/RPUSH

一次可以插入一个元素,也可以插入多个元素,按顺序头插

时间复杂度O(1)

返回值是list的长度.

sql 复制代码
LPUSH key element [element...]
1push key 1 2 3 4
--(integer) 4
1push key 5 6 7 8
--(integer) 8

LPUSHX/RPUSHX

X ,其实就是 **eXists(存在),**只有当这个 Key(列表)原本就存在时,它才会往里塞数据

时间复杂度:O(1)

返回值:list的长度

sql 复制代码
LPUSH key value [value ...]

LRANGE

Irange命令查看list 中指定范围的元素,此处描述的区间也是闭区间,下标支持负数

-1 :代表倒数下标的起点,即最后一个元素(最右侧尾部)

Java中,下标超出范围,一般会抛出异常;多出一步下标合法性的验证;缺点:速度比上面要慢优点:出现问题能及时发现

在Redis中直接尽可能的获取到给定区间的元素.如果给定区间非法,比如超出下标就会尽可能的获取对应的内容

sql 复制代码
LRANGE key start stop

lrange key 0 -1
--"8"
--"7"
--"6"
--"5"
--"4"
--"3"
--"2"
--"1"

LPOP/RPOP

头删/尾删

时间复杂度:O(1)

返回值:取出的元素或者nil

COUNT : 从redis 6.2版本,新增了一个count参;count 就描述这次要删几个元素

lindex / linsert / llen

--LLEN key(Length - 获取列表长度)

O(1)

如果这个 key 不存在,返回 0;如果这个 key 不是列表类型,报错

--LINDEX key index(Index - 按下标获取单个元素)

O(N)

Redis 的列表底层是基于链表 实现的。链表不像数组,它不能根据下标直接做数学计算定位。如果输入一个很深的索引(比如 LINDEX my_list 50000),Redis 必须从头或从尾开始一个一个节点往后数,数到第 50000 个才能拿出来。

--LINSERT key BEFORE|AFTER pivot value (Insert - 定向精准插队)

在列表里寻找第一个名字叫 pivot(基准点)的元素,然后在它的 前面(BEFORE) 或者 后面(AFTER) 强行插进去一个新值 value

O(N)

LREM

  • key:列表的名字。

  • value :你想删除的那个具体元素的值(比如要删掉 "abc")。

  • count :一个整数,用来控制删除的个数搜索的方向

  • count < 0 ------ 从右往左,删除指定个数;count = 0 ------ 全部删除

sql 复制代码
LREM key count value

O(N + k):N 是整个列表的长度,k 是被删除的元素个数

返回值是实际成功删除的元素个数

LTRIM/LSET

对一个列表进行修剪,只保留指定区间内的元素,不在区间内的元素统统删掉。

时间复杂度: O(N) ------ N 为被移除的元素个数。

核心看点 : 常用于维持一个定长列表(比如只保留最新的 100 条日志),防止列表无限增长撑爆内存。

sql 复制代码
--只保留正数下标 0 到 2 的元素(即前 3 个),其余的全部砍掉
--my_list 内部数据为:["8", "7", "6", "5", "4", "3", "2", "1"]
ltrim my_list 0 2
# 返回值:OK

--再次查看列表,发现只剩下前三个了
lrange my_list 0 -1
--"8"
--"7"
--"6"

LSET修改(设置)列表中指定下标(Index)的元素的值

时间复杂度 : O(N) ------ N 为到达指定索引需要遍历的节点数。特殊情况:修改头尾(0-1)是 O(1)。

返回值 : 成功返回 OK;如果下标越界,直接报错

sql 复制代码
LSET key index value
--假设目前 my_list 内部数据为:["8", "7", "6"]
--将正数下标为 1 的元素(原本是 "7"),强行修改为 "999"
lset my_list 1 "999"
--返回值:OK

--查看修改后的结果
lrange my_list 0 -1
--"8"
--"999"   # 已经被成功篡改
--"6"

--⚠️ 踩坑点:如果指定的下标超出了当前列表的实际长度
lset my_list 10 "error"
--返回值:(error) ERR index out of range(报错:索引越界)

BLPOP/BRPOP

Blocking(阻塞)

从列表的头部(L) 尾部(R)弹出一个元素。如果列表中有数据 ,它们的效果和普通的 LPOP / RPOP 完全一样,;但如果列表中没有数据(空列表) ,它们不会返回 nil,而是会让当前的客户端根据timeout,阻塞一段时间期间Redis可以执行其他命令

时间复杂度: O(1)

返回值: Key 的名字+弹出的值

sql 复制代码
BLPOP key [key ...] timeout

--消费者
BRPOP empty_list 20
--进入倒计时20s等待...

--生产者
LPUSH empty_list "Tesla"
--(integer) 1

--消费者,瞬间被唤醒,并打印出数据和耗时
"empty_list"   # 告诉你哪个队列来货了
"Tesla"        # 拿到的商品
(3.45s)        # 自动记录:你刚好苦苦等待了 3.45 秒

【一直没数据(超时解脱】等了 5 秒钟依然没有任何人往里面放数据,客户端会自动醒来
BRPOP empty_list 5
(nil)
(5.02s)  # 超时退出,返回 nil

此处的这俩阻塞命令用途主要就是用来作为"消息队列",整体来说,这俩命令功能还是比较有限

小结

内部编码

列表类型的内部编码有两种:

ziplist(压缩列表) :当列表的元素个数小于list-max-ziplist-entries 配置(默认512个--现在已经不再使用),同时列表中每个元素的长度都小于list-max-ziplist-value 配置(默认64字节)时,Redis会选用ziplist来作为列表的内部编码实现来减少内存消耗。

linkedlist(链表) :当列表类型无法满足ziplist 的条件时,使用linkedlist 作为列表的内部实现。

新版本quicklist:相当于是链表和压缩列表的结合,整体还是一个链表,链表的每个节点,是一个压缩列表

场景

存储表

sql 复制代码
--将张三、李四写入 1 班的 List
RPUSH class:1 '{"studentId":1,"studentName":"张三","age":20,"score":90}'
RPUSH class:1 '{"studentId":2,"studentName":"李四","age":19,"score":89}'

--将王五写入 2 班的 List
RPUSH class:2 '{"studentId":3,"studentName":"王五","age":21,"score":88}'

消息队列

多频道多通道

微博 Timeline

每个用户都有属于自己的T:hneline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

1)每篇微博使用哈希结构存储,例如微博中3个属性:title、timestamp、content:

sql 复制代码
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
... ...
 hmset mblog:n title xx timestamp 1476536196 content xxxxx

2)向用户微博列表添加微博,user:<uid>:mblogs作为微博的键:【表示用户和微博间的存储关系】

sql 复制代码
lpush user:1:mblogs mblog:1 mblog:3
... ...
lpush user:k:mblogs mblog:9

3)分页获取用户的Timeline,例如获取用户1的前10篇微博:

sql 复制代码
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}

4.关于set

set:集合中的元素是无序的:顺序不重要,变化一下顺序,集合还是那个集合

list:顺序很重要.变换一下顺序,就是不同的list了

SADD

sql 复制代码
SADD keymember[member...]
sadd key2 1 1 2 3 4
--(integer ) 4

SMEMBERS

获取一个set中的所有元素,注意,元素间的顺序是无序的

O(N)

返回所有元素的列表

sql 复制代码
SMERBERS KEY2

SISMEMBER

判断一个元素在不在set中

O(1)

返回值:1表示元素在set中。0表示元素不在set中或者key不存在

SPOP/SRANDMEMBER

随机删除coount个元素,不写count为随机删一个

sql 复制代码
SPOP key [count]

srandmember:能够随机获取多个或一个元素

SMOVE/SREM

sql 复制代码
SMOVE source destination member

把member从 source 上删除,再插入到destination中

SREM : SREM key membermember...

SINTER/SINTERSTORE

交集(inter)、并集(union)、差集(diff):差集:A和B做差集,就是找出在A中存在,在B中不存在

sql 复制代码
 SINTER key [key ...]

SINTER:获取给定set的交集中的元素

O(N*M),N是最小的集合元素个数.M是最大的集合元素个数 ; 返回值:交集的元素

sql 复制代码
SINTERSTORE key3 key key2
--对key和key2求交集,放到key3

SINTERSTORE:直接把算好的交集,放到destination这个key对应的集合中,返回个数

SUNION/SUNIONSTORE/SDIFF/SDIFFSTORE

SUNION:返回的就是并集结果

SDIFF:返回差集

SET内部编码

intset(整数集合):为了节省空间,当元素均为整数,并且元素个数不是很多的时候.

hashtable (哈希表):有整数外的存入哈希表

小结

命令 时间复杂度 说明
sadd key element [element ...] O(k) k 是元素个数
srem key element [element ...] O(k) k 是元素个数
scard key O(1) -
sismember key element O(1) -
srandmember key [count] O(n) n 是 count
spop key [count] O(n) n 是 count
smembers key O(k) k 是元素个数
sinter key [key ...] sinterstore O(m * k) k 是几个集合中元素最小的个数,m 是集合数
sunion key [key ...] sunionstore O(k) k 是多个集合的元素个数总和
sdiff key [key ...] sdiffstore O(k) k是多个集合的元素个数总和

应用场景

用户画像:使用Set来保存用户的"标签",分析出你这个人的一些特征,分析清楚特征之后,再投其所好;Set方便计算交集~很容易的找到两个用户之间的公共标签;基于这样的标签,衍生出一些"用户关系"

共同好友:使用Set来计算用户之间的共同好友~基于"集合求交集"

统计UV :. PV--page view:用户每次访问该服务器,每次访问都会产生一个pv.

UV--user view:每个用户,访问服务器,都会产生一个uv.但是同一个用户多次访问,不会使uv增加~uv需要按照用户进行去重~~上述的去重过程,就可以使用set来实现

5.关于Zset

有序集合;升序/降序

Zset 中的member 要求是唯一的(score用来排序,可以重复)

ZADD

之前Hash,Set,List很多时候,添加一个元素,都是O(1)此处,

Zset则是logN,由于zset是有序结构,要求新增的元素,要放到合适的位置上(找位置)

参数名称 可选/必选 含义与功能说明
key 必选 有序集合的键名。
NX 可选 只添加新元素,不更新已存在的元素。
XX 可选 只更新已存在的元素,不添加新元素。【NXXX 互斥
GT 可选 仅当新分数大于当前分数时,才更新已存在的元素。
LT 可选 仅当新分数小于 当前分数时,才更新已存在的元素。【NXGT / LT 互斥
CH 可选 修改返回值的行为。默认情况下,ZADD 只返回新添加的元素个数。加上 CH 后,返回值会包含新添加 以及分数被更新的元素总数。
INCR 可选 将指定元素的分数加上 score 值。在此模式下,只能指定一对 score member,且返回值会变成该元素更新后的新分数。
score 必选 元素的分数(可以是整数或双精度浮点数),用于排序。
member 必选 要添加到有序集合中的元素成员,需唯一
[score member ...] 可选 支持同时添加或更新多个"分数-成员"对(批量操作)。

小结

命令 时间复杂度 含义与参数说明
zadd key score member [score member ...] O(k *log(n)) k 是添加的成员个数,n 是当前有序集合的元素个数。
zcard key O(1) 获取当前有序集合的元素总个数。
zscore key member O(1) 获取指定成员的分数。
zrank key member zrevrank key member O(log(n)) n 是当前有序集合的元素个数。 zrank正序 (分数从小到大)排名;zrevrank倒序 (分数从大到小)排名。 (注:原图拼写错误已修正)
zrem key member [member ...] O(k *log(n)) k 是删除的成员个数,n 是当前有序集合的元素个数。
zincrby key increment member O(log(n)) n 是当前有序集合的元素个数。为指定成员的分数加上增量 increment
zrange key start end [withscores] zrevrange key start end [withscores] O(k + log(n)) k 是获取的成员个数,n 是当前有序集合的元素个数。返回指定索引区间内的成员。
zrangebyscore key min max [withscores] zrevrangebyscore key max min [withscores] O(k + log(n)) k 是获取的成员个数,n 是当前有序集合的元素个数。返回指定分数区间内的成员。
zcount key min max O(\log(n)) n 是当前有序集合的元素个数。返回指定分数区间内的成员数量。
zremrangebyrank key start end O(k + log(n)) k是删除的成员个数,n 是当前有序集合的元素个数。删除指定排名区间内的所有成员。
zremrangebyscore key min max O(k + \log(n)) k 是删除的成员个数,n 是当前有序集合的元素个数。删除指定分数区间内的所有成员。
zinterstore destination numkeys key [key ...] O(n * k) + O(m *log(m)) n 是输入集合中元素最少 的那个集合的元素个数;k 是输入的集合个数;m 是合并后目标结果集合 的元素个数。计算交集并存储。(numkeys :参与计算的源 Zset 的**数量,**必须与后面给出的 key 的数量一致)
zunionstore destination numkeys key [key ...] O(n) + O(m *log(m)) n 是所有输入集合的元素个数总和 ;m 是合并后目标结果集合的元素个数。计算并集并存储。

编码方式

ziplist :如果有序集合中的元素个数较少,或者单个元素体积较小.使用ziplist来存储.

压缩列表,节省内存空间.

skiplist跳表:当前元素个数多,或单个元素体积非常大,用 skiplist来存,时复O(logN)

应用场景

排行榜 :热搜、游戏天梯、成绩(排行榜要求实时变化

【热度:浏览量,点赞量,转发量,评论... ...(每个维度有权重分配)】

相关推荐
杜子不疼.1 小时前
Agent Skills 的演进治理与 Swarm Skills 自演进
服务器·数据库·microsoft
wanghowie1 小时前
26.v3 核心升级:语义层 + 指标体系——禁止 LLM 直连 SQL
数据库·sql
袋鼠云数栈1 小时前
数栈 V7.0 多模态数据智能平台:打造 AI-Ready 的企业数据底座
大数据·数据结构·数据库·人工智能·数据治理·多模态
Mr. zhihao1 小时前
Redis Bitmap:BitCount、bitTop的使用业务场景
数据库·redis·缓存
永远不会出bug1 小时前
PgSql数据库函数
数据库
Volunteer Technology1 小时前
Flink Sink
大数据·数据库·flink
程思扬1 小时前
Android Room 数据库跨版本升级闪退问题根治方案
android·数据库·oracle
IvorySQL1 小时前
PostgreSQL 技术日报 (5月31日)|内核功能研讨,PG 大会赛事动态
数据库·postgresql
Devin~Y2 小时前
智慧物流+AIGC客服Java大厂面试:Spring Boot、Kafka、Redis、JVM与RAG Agent实战
java·jvm·spring boot·redis·spring cloud·kafka·rag