在初期,已经讲述了Redis安装问题。现在正式进入Redis的入门阶段
系统架构的演进
传统单机架构
一台机器运行应用程序、数据库服务器
现在大部分公司的产品都是这种单机架构。因为现在计算机硬件发展速度很快,哪怕只有一台主机,性能也很高的。可以支持几万级别的高并发和庞大的数据存储。
当业务进一步增长,用户量和数据量水涨船高。一台主机难以应付的时候就需要引入更多的主机和硬件资源
服务器每次收到一个请求,都是需要消耗上述的资源。如果同一时刻请求多了,某个硬件资源不够用【木桶效应】都可能会导致服务器处理请求时间变长甚至出错。一般的解决方案也是围绕着开源节流
引入分布式
传统的单主机架构既负责应用服务又要负责存储服务当业务量增长的时候就会出现第一次的性能瓶颈,此时可以通过基础的分布式:应用和存储分离进而分散流量压力,根据业务进行自定义服务器配置
应用和数据库分离,分别部署到不同主机上
如果还需要提升性能,可以考虑引入负载均衡,合理分配流量到对应的应用服务器
引入负载均衡
如果应用服务器的CPU和内存资源吃完之后,还可以引入更多的应用服务器通过负载均衡器比较均匀的分配请求给应用服务器就可以有效解决第二次的性能瓶颈。集群中的某个主机宕机、其它的主机仍然可以承担服务提高整个系统的可用性
假设有1w个用户请求,有两个应用服务器就可以负载均衡模式就可以让每个应用服务器承担0.5w的访问量
负载均衡器就像公司的一个组的领导一样,要负责管理,把任务分配给每个组员。对于负载均衡器来说,有很多的 负载均衡 具体的算法,它对于请求量的承担能力远远超过服务器
如果出现了请求量大到负载均衡器也扛不住压力的时候可以再引入更多的负载均衡器,甚至引入硬件资源(F5)进行更大的分流
引入数据库读写分离
虽然增加应用服务器、确实可以处理更高的请求量。但是随之而来的存储服务器要承担的请求量也就更多
处理办法还是:开源+节流(门槛高,更复杂)。简单粗暴的增加一个存储服务器,将读写操作分离
一个数据库节点作为主节点负责写数据,其它N个数据库节点作为从节点负责读数据
但是数据库天然的问题就是:响应速度慢。解决方案就是进行数据区分引入缓存
引入缓存
把数据区分“冷热”、热点数据放到缓存中,缓存的访问速度比数据库很多
缓存服务器只是存放一小部分热点数据,采取二八原则【20%数据能够支持80%的访问量】
这里的缓存就用的Redis、但缺点就是内存小。从此可以看出缓存服务器扛住了大量的读请求负重前行,因此需要一个皮实的缓存服务器就可以进一步提高并发量
这里也需要考虑缓存中的数据同步问题,比如双十一商品会打折。那么主从数据库更新数据之后缓存也需要跟着修改
引入数据库分库分表
引入了分布式不仅要能够去应对高并发的请求量,同时也要能应对更大的数据量。因为依旧会存在一台服务器已经存不下数据的意外发生
我们可以针对数据进一步的分析:分库分表
一个数据库服务器上有多个数据库(逻辑上的数据集合CREATE DATABASE db_name
)。现在就可以引入多个数据库服务器,每个数据库服务器存储一个或者一部分数据库
如果某个表特别大,比如订单表。大到一个服务器存不下,就需要对表进行拆分成多个服务器进行分开存储
引入微服务
之前单个应用服务器做了很多业务,可能会导致服务器代码越来越复杂。后期为了维护方便,就可以把这样的一个复杂的服务器拆分成更多、功能更单一但是更小的服务器(微服务)
- 微服务优势
- 解决人的问题
- 使用微服务可以实现功能复用
- 可以给不同的服务进行不同的部署
- 微服务劣势
- 系统性能下降
- 拆出来更多的服务,多个功能之间更依赖网络通信,而网络通信的速度实际上可能比硬盘更慢。但现在有些 万兆网卡 的读写性能已经超过硬盘读写了
- 系统复杂程度提高,可用性受到影响
- 服务器更多了,出现问题的概率就更大了。【更丰富的监控机制、配套的运维人员】
有的业务场景适合微服务,有的不一定适合微服务(钞能力)
- 服务器更多了,出现问题的概率就更大了。【更丰富的监控机制、配套的运维人员】
- 系统性能下降
Redis特性介绍
Redis的初心是用来作为一个“消息中间件”使用(消息队列),但当前很少使用Redis作为消息中间件(业界有更多更专业的消息中间件使用)
如果单机程序、直接通过变量存储数据的方式比Redis更优。但由于进程的隔离性,需要通过“网络”进行进程间通信
通常互联网的热点数据也会遵守一个“二八原则”:20%的热点数据能满足80%的访问需求
打开官网就会发现它的特性
In-memory data structures
- MySQL主要是通过 “表” 的方式来存储组织数据【关系型数据库】
- Redis主要是通过 “键值对” 的方式来存储组织数据【非关系型数据库】
- key都是string,value则可以使上述的这些数据结构
Programmability
- Lua也是一个编程语言。针对Reis操作,可以直接通过简单的交互式命令进行操作也可以通过一些脚本的方式批量执行一些操作(可带有一定逻辑)
Extensibility
- 通过这些语言可以编译出Redis扩展【win的dll、linux的.so】
- 可以扩展出Redis支持一个搜索二叉树的数据结构
- 通过这些语言可以编译出Redis扩展【win的dll、linux的.so】
Persistence
-
- Redis把数据存储在内存上,当进程/系统重启内存的数据就会丢失。为此Redis会把数据存储的硬盘上。内存为主、硬盘为辅
Clustering
-
- Redis作为一个分布式系统的中间件,能够支持集群是很关键的。这个水平扩展类似于“分库分表”
High availability
-
- 高可用也可以换做说话冗余/备份
- Redis本身也是支持“主从”结构,从节点就相当于主节点备份,主节点挂掉的话从节点就会充当主节点
为什么Redis速度很快
-
Redis数据在内存中,就比访问硬盘的速度快很多
-
Redis的核心功能都是比较简单的逻辑:操作内存的数据结构
-
从网络角度上,Redis使用了IO多路复用的方式(epoll)
- 使用一个线程,管理多个socket
-
Redis使用的是单线程模型
- 高版本的Redis引入了多线程,但也只是网络IO中用到了多线程。核心业务逻辑依旧是单线程
- 这样的单线程模型减少了不必要的线程之间的竞争开销
- 这可能和之前的人之中多线程提高业务效率有悖论
-
Redis是用C语言开发的所以就快
应用场景
Real-time data store
把Redis当作数据库使用:存全量数据
- Redis’ versatile in-memory data structures enable building data infrastructure for real-time applications that require low latency and high-throughput.
在线搜索引擎项目中构建的一些索引数据结构
Caching & session storage
把Redis当作缓存使用:存热点数据/会话
- Redis’ speed makes it ideal for caching database queries, complex computations, API calls, and session state
- 想办法让负载均衡器把同一个用户的请求时中达到同一个服务器【不采取轮询,而是通过userID求余之类的方式来分配服务器】
- 把会话单独拎出来,放到Redis服务器上进行保存。这样每次请求,ABC服务器都会从Redis服务器上拿会话
Streaming & messaging
消息队列
- The stream data type enables high-rate data ingestion, messaging, event sourcing, and notifications
- 可以实现一个网络版本的生产者消费者模型。对于分布式系统来说,服务器之间有时候也需要使用到生产者消费者模型【解耦合、削峰填谷】
Redis最不能做的事情就是存储大规模的数据
通用命令
redis中的命令不区分大小写
keys
-
作用:通过正则查询当前服务器上匹配的key
-
语法
-
KEYS pattern set hallo 1 set hbllo 1 set hcllo 1 set hddllo 1 set heeello 1 set habcdello 1 keys h?llo keys h*llo keys h[abcde]llo keys h[a-e]llo # 由于 heeello 中间是3个e而不是单独的字符,所以无法匹配 keys h[^ab]llo # 生产环境禁止查询全部key keys *
-
h?llo
matcheshello
,hallo
andhxllo
-
h*llo
matcheshllo
andheeeello
-
h[ae]llo
matcheshello
andhallo,
but nothillo
-
h[^e]llo
matcheshallo
,hbllo
, … but nothello
-
h[a-b]llo
matcheshallo
andhbllo
-
-
-
时间复杂度:O(N)
-
注意
未来工作中会涉及到的几个环境
- 办公环境
- 入职之后,公司给你发的个人办公电脑
- 开发环境
- 有时候开发环境和办公环境是同一个【前端/客户端】
- 也有时候开发开发环境是单独的服务器【后端】
- 有的后端程序很有可能是单独的服务器,编译一次时间特别久
- 有的后端程序已启动要消耗很多的 cpu 和 内存 资源
- 有的程序比较依赖 Linux,在 Windows 环境中搭不起来
- 测试环境
- 测试工程师使用的【一般配置都很高】
- 线上环境/生产环境
- 办公环境
exists
- 作用:判断某个 key 是否存在
- 语法
-
EXISTS key [key ...] exists hbllo exists hallo exists hbllo hallo ```
-
- 时间复杂度:O(1)
- 返回值:key 存在的个数
del
- 作用:删除指定的 key
- 语法
-
DEL key [key ...] del hallo del hbllo hcllo hzllo ``` > Redis的删除操作危险程度远低于MySQL的删除操作(DROP DATABASE、DROP TABLE、DELETE FROM)。Redis主要应用场景就是作为缓存,此时Redis里存的只是一个热点数据,全量数据是在MySQL数据库中。相比之下如果是MySQL这样的数据,哪怕删除了一条数据,都可能影响很大 > <font color=skyblue>作为缓存,如果Redis大半的数据没了,这种影响会很大</font> > <font color=skyblue>作为数据库,如果Redis误删数据,这种影响会很大</font> > <font color=skyblue>作为MQ消息队列,如果Redis误删数据,这种影响需要具体问题具体分析</font>
-
- 时间复杂度:O(1)
- 返回值:删除掉的 key 的个数
expire
- 作用:给指定的 key 设置过期时间
- 语法
EXPIRE key seconds [NX | XX | GT | LT] expire hzllo 10 expire habcdello 5 get habcdello # 对于计算机来说:秒 是一个非常长的时间 PEXPIRE key milliseconds [NX | XX | GT | LT] ```
- 时间复杂度:O(1)
- 返回值:1表示设置成功、0表示设置失败
ttl
- 作用:time to live。剩余的过期时间
- 语法
TTL key expire hddllo 10 ttl hddllo get hddllo # 如果对时间有更高的精度,则可以使用 PTTL PTTL key ```
- 时间复杂图:O(1)
- 返回值:剩余过期时间(s)。-1:key无过期时间,-2:key不存在
type
- 作用:返回 key 对应的数据类型
- none、string、list、set、zset、hash 和 stream
- stream:Redis 当消息队列的时候
- none、string、list、set、zset、hash 和 stream
- 语法
-
TYPE key type key set key 1 type key lpush key2 1 2 1 2 type key2 sadd key3 1 2 1 2 type key3 hset key4 field1 value1 type key4
-
- 时间复杂度:O(1)
- 返回值:key 对应的数据类型
key的过期策略
redis 的 key 过期策略是怎么实现的?
一个 reids 中可能同时存在很多 key,这些 key 中可能有很大一部分有过期时间。此时 redis 服务器如何知道哪些 key 已经过期要被删除,哪些 key 还未过期呢?
redis过期策略主要分为两大类
-
定期删除
-
每次抽取一部分验证过期时间,保证这个抽取检查的过程足够快
-
-
惰性删除
- 假设 key 已经到过期时间但暂时还没把它删除紧接着后面又一次访问正好用到了这个 key,于是这次访问就会让 redis 服务器触发删除 key 的操作,同时再返回一个 nil
虽然有了上述两种策略结合,但整体效果一般。仍然可能会有很多过期的 key 被残留,没有被及时清理
因此 redis 又提供了一些列的内存定期淘汰策略
定时器的实现原理
定时器:在某个时间到达之后执行指定任务
-
优先级队列
实现原理:把要执行的任务放入优先级队列中,此时定时器中只要分配一个线程,让这个线程去检查队首元素是否过期
扫描线程检查队首元素是否过期时候也不能太频繁,因为会无缘无故消耗很多CPU资源。优化方案: 根据当前时间和队首元素设置一个等待时间,当时间到之前唤醒此扫描线程
一个利用阻塞队列+线程模拟实现的定时器
package src;
import java.util.concurrent.PriorityBlockingQueue;
class MyTimer {
static class Task implements Comparable<Task> {
//1.执行具体的任务
private Runnable runnable;
//2.执行任务等待的时间
private long time;
public Task(Runnable runnable, long time) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + time;
}
@Override
public int compareTo(Task o) {
return (int) (this.time - o.time);
}
public void run() {
this.runnable.run();
}
}
//3.把任务组织在一起
private PriorityBlockingQueue<Task> tasks = new PriorityBlockingQueue();
// 4.往定时器中加任务
public void schedule(Runnable runnable, long after) {
Task task = new Task(runnable, after);
tasks.put(task);
}
// 4.创建一个扫描线程,扫描队首元素
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
Task task = tasks.take();
long cur_tim = System.currentTimeMillis();
if (cur_tim <= task.time) {
tasks.put(task);
synchronized (locker) {
locker.wait(task.time - cur_tim);
}
} else {
task.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public class TimerPrinciple {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.printf("%s任务执行完毕\n", Thread.currentThread().getName());
}
};
long after = 1000;
System.out.printf("%s线程开始执行,%s 秒后开始执行Task任务\n", Thread.currentThread().getName(), after / 1000);
myTimer.schedule(runnable, after);
}
}
- 时间轮
- 箭头相当于一个函数指针【指向函数的指针】以及对应的参数
- 时间划分成很多小段,划分的粒度看实际需求
但Redis的定时器并未按照上述两种方案进行设计,初衷可能是不引入多线程。但上述两种方案是比较高效的实现方式,很多场景也会用到
数据类型和编码方式
圈红的是常用的
Redis底层在实现上述数据结构的时候会在源码层面进行优化来达到 节省时间/空间 效果。
keys *
type key
get key
object encoding key
type key3
object encoding key3
type key3
object encoding key3
type key4
object encoding key4
单线程模型的工作过程
单线程并非是Redis服务器内部只有一个线程,而是只有一个线程处理所有的命令请求。其实Redis内部有多个线程,这些线程处理网络IO
当前这两个客户端 “并发” 的发起了上述的请求,是否会有线程安全问题呢?
答案:并不会。Redis 服务器实际上是单线程模型,保证了当前收到的这个请求是串行执行的
单线程为什么快
这里的快参照物是数据库(MySQL,Oracle,SQLServer等)
- Redis 访问内存,数据库访问的是硬盘
- Redis 核心功能比数据库核心功能更简单
- 数据库对数据的CRUD都有更复杂的功能支持,势必会花费更多的开销【主外键约束、唯一性索引等】
- 采取单线程模型,避免了一些不必要的线程竞争
- Redis 每个基本操作都是 短平快 简单的操作一下内存,不是什么特别消耗 CPU 的操作,就算做多线程提升也不大
- 处理网络IO的时候用的 epoll 这样的 IO 多路复用机制
- 一个线程就可以管理多个 socket ,针对 TCP 来说,服务器每次服务一个客户端需要给客户端安排一个 socket,这些 socket 上并非都无时不刻的传输数据【很多情况下客户端和服务器之间的通信也没那么频繁,直播游戏除外】
- 同一时刻只有少数 socket 是活跃的
- 一个线程就可以管理多个 socket ,针对 TCP 来说,服务器每次服务一个客户端需要给客户端安排一个 socket,这些 socket 上并非都无时不刻的传输数据【很多情况下客户端和服务器之间的通信也没那么频繁,直播游戏除外】
5种常用数据类型
整体上来说Redis是键值对结构,key 固定就是字符串,value实际上会有多种类型
- 字符串
- 哈希表
- 列表
- 集合
- 有序集合
-
Redis 中的字符串按照二进制数据的方式存储(不会做任何编码转换)
-
文本字符串,xml,json,帧数,二进制数据(图片/音频/视频)
-
-
一般来讲,Redis乱码概率很小
string类型
String字符串是Redis最简单的存储类型。
根据字符串格式不同,可以分为3类
- String:普通字符串
- int:整形,可自增、自减操作
- float:整形,可自增、自减操作
不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512MB
KEY | VALUE |
---|---|
name | 张三 |
num | 1 |
price | 1.1 |
语法 | 含义 |
---|---|
SET | 添加/修改 已经存在的一个String 类型的键值对 |
GET | 根据 KEY 获取 VALUE |
MSET | 批量添加多个 String 类型键值对 |
MGET | 根据多个 KEY 获取多个 String 类型的 VALUE |
INCR | 让一个整形的 KEY 自增/自减 |
INCRBY | 让一个整形的 KEY 自增指定大小【INCRBY num -2:num -= 2】 |
INCRBYFLOAT | 让一个浮点型数据自增 |
SETNX | 添加一个 String 类型键值对,前提是这个 KEY 不存在,否则不执行 |
SETEX | 添加一个 String 类型键值对,并且指定有效期 |
KEY 结构
Redis没有MySQL中Table表的概念。如何区分不同类型的 KEY 呢?
比如存储一个ID都为1的用户数据和文章数据,那么 SET ID 1
就会冲突。解决方案是:多个单词之间用 : 分隔开,格式如下:
user相关的key:BlogSystem:user:1
文章相关的key:BlogSystem:article:1
如果VALUE是一个对象,则可以将对象序列化为JSON字符串后存储
KEY | VALUE |
---|---|
BlogSystem:user:1 | {“id”:1, “name”: “张三”, “age”:13} |
BlogSystem:article:1 | “id”:1, “title”: “Redis快速入门”, “updateTime”: “2022-12-23” |
-
set 和 get
-
set
-
语法
-
SET key VALUE [expiration PX seconds|PX milliseconds] [NX|XX] FLUSHALL keys * set key1 1 set key2 2 ex 5 ttl key2 set key2 2 NX set key1 1 NX get key1 set key1 111 XX set key3 333 XX exists key3
-
时间复杂度:O(1)
-
-
将两步操作并为一步
-
set key1 value1 expire value1 10 set key1 value1 ex 5
-
-
-
-
get
-
语法
-
RPUSH key4 11 22 33 get key4 type list
-
-
时间复杂度:O(1)
-
注意
-
get 只支持字符串类型的 value,如果 value 是其它类型,使用 get 就会出错
-
-
-
-
mset 和 mget
-
-
MSET
MSET key value [key value ...] mset key1 111 key2 222 key3 333
-
MGET
MGET key [key ...] mget key1 key2 key3
-
时间复杂度:O(N)
- N:命令中 key 的个数,可以认为是 O(1)
-
-
setnx、setex、psetex
-
语法
-
SETNX SETEX PSETX FLUSHALL setnx key1 111 get key1 setnx key1 222 get key1 setex key2 5 222 ttl key2 get key2 psetex key3 5000 333 pttl key3
-
-
-
incr,incrby和incrbyfloat
-
incr
-
作用:针对value+1
-
语法
-
INCR key FLUSHALL set key 10 incr key set key "hello" incr key set key 1.2 incr key set key 18446744073709552000 incr key get key2 incr key2
2 64 = 18446744073709552000 2^{64}=18446744073709552000 264=18446744073709552000,Redis中的 insteger 不能超过此范围
-
-
时间复杂度:O(1)
-
返回值:+1 之后的值【++i】
-
注意
-
-
incrby
-
作用:针对value+n
-
语法
-
incrby key n get key2 incr key2 incrby key2 9 get key3 incrby key3 incrby key3 0 incrby key3 -1 get key3
-
-
返回值:+n之后的值【+=n】
-
-
incrbufloat
-
作用:针对value +/- 小数
-
语法
-
set key 1.1 xx incrbyfloat key 0.00000000000001 incrbyfloat key -0.00000000000001
-
-
返回值:+小数之后的值
-
注意
-
-
-
decr 和 decrby
- decr:针对value-1
- decrby:针对value-n
-
其它字符串操作
-
append
-
作用
- key存在且是一个string,会在原有字符串末尾追加新string
- key不存在 :效果等同于 SET
-
语法
-
APPEND key value FLUSHALL append key hello get key append key2 你好
-
-
时间复杂度:O(1)
-
返回值:追加完成之后string的长度【返回的是字节数】
-
-
getrange
-
作用:获取子串
-
语法
-
GETRANGE start end get key getrange key 0 -1 getrange key 0 -2
-
-
时间复杂度:O(N)
-
返回值:string类型的字串
-
注意
- 左闭右闭区间
- 超过范围的偏移量会根据string长度调整成正确的值
- -1:倒数第一个,-2:倒数二个
- 如果是汉字,则容易出错
-
-
setrange
-
作用:替换字符串
-
语法
-
SETRANGE key offset value FLUSHALL set key helloworld nx setrange key 1 aaa get key setrange key 1 bbbb get key setrange key 1 zzzzzzzzzzz get key setrange key2 1 aaa get key2
-
-
针对不存在 key时,windows下没有效果也不报错。而Linux中则正常,偏移1个量后续追加3个a。之前的内容会由 \x00 填充【\x00aaa】
-
-
-
时间复杂度:O(N)
-
返回值:替换之后新字符串长度
-
注意
- 同样中文无法解决
-
-
strlen
-
作用:获取字符串长度【单位是字节】
-
语法
-
STRLEN key FLUSHALL set key helloworld strlen key strlen key1
-
-
时间复杂度:O(1)
-
返回值:string的长度,如果key不存在则返回0
-
注意
- 如果key存放的类型不是string时就报错
-
-
-
string类型编码方式
字符串类型的内部编码有3中:
- int:8个字节的长整型
- embstr:小于等于39个字节的字符串
- raw:大于39个字节的字符串
Redis会自动调整对应字符串的编码方式
FLUSHALL set key 123 object encoding key set key2 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq object encoding key2 set key3 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq object encoding key3 set key4 1.1 object encoding key4
-
string类型应用场景
-
缓存
Redis+MySQL组成的缓存架构
Redis缓存经常会存储 热点数据
上述业务中随着时间退役,Redis会有越来越多的数据。因此就Redis诞生了内存淘汰策略,我们一般在写入数据的时候会加给key设置一个过期时间来防止数据积累
public UserInfo getUserInfo(long uid) { // 根据 uid 得到 Redis 的 key String key = "user:info:" + uid; // 尝试从 Redis 中获取值 String vlaue = Redis执行命令.get(key); // 如果缓存命中(hit) if (value != null) { // 把用户数据反序列化 UserInfo userInfo = JSON反序列化(value); return userInfo; } // 如果缓存未命中(miss) if (value == null) { // 从数据库中,根据 uid 获取⽤⼾信息 UserInfo userInfo = MySQL 执⾏SQL:select * from user_info where uid = <uid >; // 如果表中没有 uid 对应的⽤⼾信息 if (userInfo == null) { 响应 404 return null; } // 将⽤⼾信息序列化成 JSON 格式 String value = JSON 序列化(userInfo); // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒) Redis 执⾏命令:set key value ex 3600 // 返回⽤⼾信息 return userInfo; } }
-
计数(Counter)功能
记录视频播放次数
为什么使用Redis统计而不用MySQL?
什么是异步写入?
实际开发过程中还要考虑很多:防作弊【此用户单个视频刷了成百上千遍】、按照不同维度统计【用户点进去就滑走】、避免单点登录问题【某台服务器挂掉后用户需要重复登陆】、数据持久化到底层数据源【不能服务器重启就丢失数据】
-
Session会话
Cookie:浏览器存储数据机制
Session:服务器存储数据的机制
左边:如果每个服务器只存储自己的会话信息不共享,当用户请求到不同服务器上就会可能出现不能处理的情况
右边:此时所有的会话信息都存储到Redis,多个服务器共享此Redis数据
-
手机验证码
很多应用出于安全考虑,安全登陆的时候让用户输入手机号再发送验证码短信,再让用户输入验证码从而验证是本人
public String SendVerificationCode(String phoneNumber) { String key = "shortMsg:limit:" + phoneNumber; boolean flag = Redis执行命令:set key 1 ex 60 nx; if (flag == false) { // 说明之前手机设置过严证码 long c = Redis执行命令:get key; if (c > 5) { //说明一分钟发送超过5次,限制发送 return null; } } //之前没有发送过验证码,设置随机的6位字符串验证【一般是数字验证码:(int) (((Math.random()) * 9 + 1) * 100000)】 String validationCode = randomCharacterGenerator(); // 5分钟(300s)有效 String validationKey = Redis执行命令:set "validation" + phoneNumber validationCode ex 300; return validationCode; } private String randomCharacterGenerator() { long timeStamp = System.currentTimeMillis(); Random random = new Random(timeStamp); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < 6; i++) { int randomInt = random.nextInt(26) + 97; char randomChar = (char) randomInt; String randomString = Character.toString(randomChar); stringBuilder.append(randomString); } return stringBuilder.toString(); } public boolean verifyVerificationCOde(String phoneNumber, String verificationCode) { // 1.先从Redis中获取对应的验证 String validationKey = "validation" + phoneNumber; String value = Redis执行命令:get validationKey; if (value == null) { // 没有发送过验证码或者验证码过期 return false; } if (value.equals(verificationCode)) { return true; } else { return true; } }
-
-
什么是业务
- 公司/产品 如何解决一个/一系列 问题,而解决问题的过程就是业务
- 当技术手段无法优化的时候可以考虑优化业务
- 典型的12306例子:放票分开时间段发放而不是某个时间段发放全国的票
hash类型
String结构将对象序列化为JSON格式存储后,当需要修改某个字段是很不方便。
Hash结构可以将对象字段单独存储,方便修改
KEY | FILED | VALUE |
---|---|---|
BlogSystem:user:1 | name | “张三” |
BlogSystem:user:1 | age | 13 |
BlogSystem:user:2 | name | “李四” |
BlogSystem:user:2 | age | 14 |
Hash常用语法
语法 | 含义 |
---|---|
HSET key field value | 添加或者修改hash类型key的field的值 |
HGET key field | 获取一个hash类型key的value |
HMSET | 批量添加多个hash类型key的field的值 |
HMGET | 批量获取多个hash类型key的field的值 |
HGETALL | 获取一个hash类型的key中的所有的field和value |
HKEYS | 获取一个hash类型的key中所有的field |
HINCRBY | 让一个hash类型key的value自增指定步长 |
HSETNX | 添加一个hash类型的key的field之,前提是这个field不存在否则不执行 |
H系列命令必须要保证 key 对应的 value 是 哈希 类型
-
hset、hget、hexists和hdel
-
hset
-
作用:设置 hash 中指定的字段(field)和值(value)
-
语法
-
HSET key field value [field value ...]
-
-
时间复杂度:O(1)
-
返回值:添加的字段个数
-
注意:HSET 已经支持同时设置多个【Redis也提供了HMSET】
-
-
hget
-
作用
-
语法
-
HGET key field FLUSHALL hset key f1 111 hset key f2 222 f3 333 f4 444 hget key f1 hget key1 f1 hget key f5
-
-
时间复杂度:O(1)
-
注意
-
-
hexists
-
作用:判断 hash 中是否有指定的字段
-
语法
-
HEXISTS key field hexists key f1 hexists key1 f1 hexists key f5
-
-
时间复杂度:O(1)
-
返回值:0:不存在、1:存在
-
-
hdel
-
作用:删除 hash 中指定的字段
-
语法
-
HDEL key field [field ...] hdel key f1 hexists key f1 hdel key f2 f3 hget key f2 hget key f3 hget key f4 hget key2 f1
-
-
时间复杂度:O(N),N:删除的字段个数
-
返回值:成功删除字段个数
-
注意
- DEL 删除的是 KEY
- HDEL 删除的是 FIELD
-
-
-
hkeys和hvals
-
hkeys
-
作用:获取 hash 中全部字段
-
语法
-
HKEYS key FLUSHALL hset key f1 111 f2 222 f3 333 f4 444 hkeys key
-
-
时间复杂度:O(N),N字段个数
-
返回值:字段列表
-
注意:不要触发 KEYS * 这种类似效果
-
-
hvals
-
作用:获取 hash 中所有的值
-
语法
-
HVALS key hvals key
-
-
时间复杂度:O(N),N:元素个数
-
返回值:所有的值
-
-
-
hgetall和hmget
-
hgetall
-
作用:获取 hash 中所有字段及对应的值
-
语法
-
HGETALL key hgetall key
-
-
时间复杂度:O(N),N:元素个数
-
返回值:字段和对应的值
-
-
hmget
-
作用:一次获取 hash 中多个字段的值
-
语法
-
HMGET key field [field ...] hmget key f1 f2 f3 f5
-
-
时间复杂度:O(N),N:元素个数
-
返回值:字段对应的值或者 nil
-
上述 hkeys,hvals,hgetall 都是一次性获取全部。因此需要用渐进式遍历 hscan,运行一次遍历一小部分再运行再遍历一小部分,连续多次就可完成整个遍历过程
-
-
hlen、hsetnx、hincr、hincrby和hincrbyfloat
-
hlen
-
作用:获取 hash 中的所有字段的个数
-
语法
-
HLEN key hgetall key hlen key
-
-
时间复杂度:O(1)
-
返回值:返回字段个数
-
-
hsetnx
-
作用:在字段不存在的情况下,设置 hash 中的字段和值
-
语法
-
HSETNX key field value hsetnx key f5 555 hsetnx key f5 666
-
-
时间复杂度:O(1)
-
返回值:0 表示失败;1表示设置成功
-
-
hincrby
-
作用:将 hash 中字段对应的数值添加指定的整数值
-
语法
-
HINCRBY key field increment hincrby key f1 0.01 hincrby key f1 10 hget key f1
-
-
时间复杂度:O(1)
-
返回值:该字段变化之后的值
-
-
hincrbyfloat
-
作用
-
语法
-
HINCRBYFLOAT key field increment hincrbyfloat key f1 0.01 hincrbyfloat key f1 -0.01
-
-
时间复杂度:O(1)
-
返回值:该字段变化之后的值
-
-
-
hash内部编码
哈希表编码方式主要有两种,ziplist和hashtable
- ziplist
- 当哈希元素个数小于 hash-max-ziplist-entries 配置(默认512)同时所有值都小于 hash-max-ziplist-value 配置(默认64字节),Redis会使用内部ziplist作为哈希的内部实现更加紧凑的数据结构,所以会节省很多空间
- hashtable
- 当哈希类型无法满足 ziplist 条件时,Redis会使用内部hashtable作为哈希内部实现,因为此时ziplist读写效率会下降而hashtable的读写时间复杂度为O(1)
- ziplist
-
哈希应用
-
作为缓存
-
关系型数据表保存用户信息
-
映射关系表示用户信息
-
-
相较于JSON格式字符串存储用户数据,哈希类型显得更直观,并且操作起来更灵活。在每个用户后面的ID作为后缀,多对field-value对应用户属性
-
public UserInfo getUserInfo(long uid) { // 1.根据 uid 得到 Redis的key String key = "user:" + uid; // 2.根据 key 查询 value UserInfoMap userInfoMap = Redis执行命令: hgetall key; // 3.如果缓存命中 if (value != null){ UserInfo userInfo = 利用映射关系构建对象(userInfoMap); return userInfo; } // 4.缓存未命中,则从MySQL中取数据 UserInfo userInfo = MySQL执⾏SQL: select * from user_info where uid = <uid>; if (userInfo == null){ 响应 404; return null; } // 5.将缓存以哈希类型进行保存 Redis执行命令: hset key name userInfo.name age userInfo.age city userInfo.city; // 6.设置过期时间位1小时 Redis执行命令: expire key 3600; return userInfo; }
-
-
-
数据库稀疏性对比
-
- 哈希类型是稀疏的,二关系型数据库是完全结构化的。例如哈希类型每个键可以有不同的 field,而关系型数据库 一旦添加新的列,所有行都要为其设置新值【包含null】
- 关系型数据库可以做复杂的查询,而Redis无法去模拟实现多表联查,聚合查询等复杂拆线呢,维护成本太高
- 对于 uid 再存储一份也可以省下来,但是如果存储的话后续开发代码的话会更方便
-
-
-
-
缓存方式对比
-
原生字符串类型:每个属性一个键
set user:1:name James set user:1:age 23 set user:1:city Beijing
- 优点:实现简单,针对个别属性变更也很灵活
- 缺点:占用过多的键导致内存占用过大,用户信息在Redis中比较分散,缺少内聚性因此没什么实用性
-
序列化字符串JSON格式
set user:1 经过序列化后的用户对象字符串
- 优点:针对总是以整体位操作的数据比较合适,编程简单。同时如果序列化方案合适,内存使用效率也很高
- 缺点:本身序列化和反序列化有一定开销,同时如果操作个别属性会不方便
-
哈希类型
hmset user:1 name James age 23 city Beijing
- 优点:简单、直观、灵活。尤其是针对局部信息变更或者获取操作
- 缺点:需要控制 ziplist 和 hashtable 两种编码方式转换,否则可能会造成较大消耗
-
-
list类型
列表(List)相当于数组或者顺序表【并非是一个简单数组二十一个更接近于双端队列deque】,两端可以插入(push)或者弹出(pop),还可以获取指定范围的元素列表
因为当前的 List 头和尾都能高校插入删除元素、就可以把这个 List 当作一个 栈/队列 来使用
Redis中的List类型与Java中的LinkedList类似,可以看作是一个双向链表结构。既可以支持正向检索也支持反向检索。
特征与LinkedList类似:
- 有序
- 元素可以重复
- 插入和删除快
- 查询一般
常用语法
语法 | 含义 |
---|---|
LPUSH key element | 在列表左侧插入一个或多个元素 |
RPUSH key element | 向列表右侧插入一个或多个元素 |
LPOP key | 移除并返回列表左侧的第一个元素,没有则返回nil |
RPOP key | 移除并返回列表右侧的第一个元素,没有则返回nil |
BLPOP和BRPOP | 与LPOP和RPOP类似,只不过在没有元素时等待指定时间而不是直接返回nil |
LRANGE key star end | 返回一段表范围内的所有元素【0下标开始计算】 |
-
lpush和lrange
-
lpush
-
作用:一个或多个元素从左侧插入【头插】
-
语法
-
LPUSH key element [element ...]
-
-
时间复杂度:插入一个元素:O(1);插入N个元素O(N)
-
返回值:插入之后 list 长度
-
注意:如果 key 已经存在,并且 key 对应的 value 类型不是 list则 lpush 会报错
-
-
lrange
-
作用:获取从 start 到 stop 区间的所有元素,左闭右闭
-
语法
-
LRANGE key start stop FLUSHALL lpush key 1 2 3 4 lpush key 5 6 7 8 lrange key 0 -1 lrange key 0 100
-
-
时间复杂度:O(N)
-
返回值:指定区间的元素
-
注意
-
-
-
lpushx,rpush,rpushx
-
lpushx
-
作用:在 key 存在时,将⼀个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回
-
语法
-
1 LPUSHX key element [element ...] FLUSHALL lpush key 1 2 3 4 lpushx key 5 6 7 8 lrange key 0 -1 keys * lpushx key2 1 2 3 4 lrange key2 0 -1 exists key2
-
-
时间复杂度:插入一个元素:O(1);插入N个元素:O(N)
-
返回值:返回值是 list 长度
-
注意
- 针对不存在的 key 进行插入则会失败,相较于 lpush 多了一层 exists 判断key是否存在
- x:是exists的意思
-
-
rpush
-
作用:将⼀个或者多个元素从右侧放入(尾插)到 list 中
-
语法
-
RPUSH key element [element ...]
-
-
时间复杂度:插入一个元素为O(1);插入N个元素为O(N)
-
返回值:插入后 list 的长度
-
-
rpushx
-
作用:在 key 存在时,将⼀个或者多个元素从右侧放入(尾插)到 list 中。
-
语法
-
RPUSHX key element [element ...]
-
-
时间复杂度:插⼊一个元素为O(1);插⼊N个元素为O(N)
-
返回值:插入后 list 的长度
-
-
-
lpop和rpop
-
lpop
-
作用:从 list 左侧取出元素【头删】
-
语法
-
LPOP key [count] FLUSHALL rpush key 1 2 lpop key lpop key lrange key 0 -1 lpop key
-
-
时间复杂度:O(1)
-
返回值:取出的元素或者nil
-
-
rpop
-
作用:从 list 右侧取出元素【尾删】
-
语法
-
RPOP key [count]
-
-
时间复杂度:O(1)
-
返回值:取出的元素或者nil
-
注意
- Linux下,从 redis6.2 版本增加了一个 count 参数,代表这次要删除多少个元素
-
-
-
lindex,linsert,llen
-
lindex
-
作用:获取下标元素值
-
语法
-
LINDEX key index FLUSHALL rpush key 1 2 3 4 5 6 7 8 9 lindex key 3 lindex key -1 lindex key 10
-
-
时间复杂度:O(N)【由于是list所以是N】,N:list中元素个数
-
返回值:获取下标元素的值
-
注意
- 下标 0 开始
- 如果下标非法,返回nil
-
-
linsert
-
作用:
-
语法
-
LINSERT key <BEFORE | AFTER> pivot element linsert key before 4 4 linsert key before 4 401 linsert key after 4 402 lrange key 0 -1
-
-
时间复杂度:O(N)
-
返回值:插入之后新的list长度
-
注意:只会在遇到的第一个元素进行插入
-
-
llen
-
作用:获取list长度
-
语法
-
LLEN key
-
-
时间复杂度:O(1)
-
返回值:list长度
-
-
-
lrem
-
作用:【remove】
-
语法
-
LREM key count element FLUSHALL rpush key 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 lrem key 2 1 lrange key 0 -1 FLUSHALL rpush key 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 lrem key -2 1 lrange key 0 -1 FLUSHALL rpush key 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 lrem key 0 1 lrange 0 -1
-
-
时间复杂度:O(N+M)
-
返回值:被删除元素个数
-
-
ltrim,lset
-
ltrim
-
作用:保留 [start, stop] 闭区间内的元素【区间外的元素被删除】
-
语法
-
LTRIM key start stop FLUSHALL rpush key 1 2 3 4 5 6 7 8 ltrim key 2 5 lrange key 0 -1
-
-
时间复杂度:O(N),N:删除个数
-
-
lset
-
作用:
-
语法
-
LSET key index element FLUSHALL rpush key 1 2 3 4 5 lset key 0 100 lset key 5 600
-
-
时间复杂度:O(N),N:list元素个数
-
注意:如果下标越界,则会报错
-
-
-
blpop和brpop
阻塞版本命令:blpop,brpop分别是lpop,rpop的阻塞版本
redis中的list也相当于阻塞队列一样,线程安全是通过单线程模型支持的,阻塞:则只支持“队列为空”的情况而不考虑“队列为满”的情况
如果 list 中存在元素:blpop,brpop和 lpop,rpop 作用完全相同
如果 list 为空:blpop和brpop就会产生阻塞,一直阻塞到队列不为空为止【但一般不提倡无休止的等】
队列空 lpop,rpop nil blpop,brpop 根据timeout阻塞一段时间,阻塞期间redis可以执行其它命令 blpop和brpop都是可以同时去尝试获取多个 key 的列表的元素。命令中如果设置了多个键,它会从左至右进行遍历键,一旦有一个键对应的列表可以弹出元素,命令就返回哪个元素。
如果多个客户端同时多个键执行 bl/rpop,则最先执行命令的客户端会得到弹出的元素
-
blpop
-
作用:
-
语法
-
BLPOP key [key ...] timeout FLUSHALL rpush key 1 blpop key 60 blpop key 60 rpush key 1 FLUSHALL blpop key key2 key3 key4 500 rpush key 1 2 rpush key2 1 2 rpush key3 1 2 rpush key4 1 2
-
-
时间复杂度:O(N),N:list元素个数
-
返回值:相当于一个pair(二元组)【数据来自哪个key、数据是什么】
-
注意:timeout 时间单位是:s【redis5:时间为整数】
-
-
brpop
-
作用:
-
语法
-
BRPOP key [key ...] timeout
-
-
时间复杂度:O(N),N:list元素个数
-
-
-
list内部编码
list列表内部编码方式也有两种
-
ziplist(压缩列表):列表元素个数小于 list-max-ziplist-entries 配置(默认 512 个)。redis会选用ziplit作为列表内部编码方式来减少内存消耗
-
-
linkedlist(链表):list类型无法满足 ziplist 时就会使用 linkedlist 作为列表内部实现
-
quicklist:相当于list和linkedlist的结合。整体还是一个链表,每个节点是一个压缩列表。
-
-
list应用场景
-
用 list 作为 “数组” 这样的结构来存储多个元素
-
消息队列
-
阻塞消息队列模型
-
-
多个列表/分频道阻塞消息队列模型
-
- 通道1:传输短视屏数据
- 通道2:传输弹幕
- 通道3:传输点赞、转发和收藏等数据
- 通道4:传输评论
- …
-
-
-
微博TimeLine(微博列表)
每个用户都有属于自己的Timeline,现在需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素
-
假设每篇微博都有 title,timestamp和content属性
-
hmset myblog:1 title redis从入门到入坟 timestamp 1698656594345 content "Redis架构演进" hmset myblog:2 title Oracle从入门到入坟 timestamp 1698656293856 content "Oracle基础语法" hmset myblog:3 title PLSQL从入门到入坟 timestamp 1698660209945 content "PLSQL存储过程" hmset myblog:4 title Spring从入门到入坟 timestamp 1698660931000 content "Spring入门"
-
-
用户发布4条微博
-
lpush user:1:myblogs myblog:1 myblog:2 myblog:3 myblog:4
-
-
分页获取用户微博
-
keyList = lrange user:1:myblogs 0 1 for key in keyList{ hgetall key } keyList = lrange user:1:myblogs 2 3 for key in keyList{ hgetall key }
此方案存在一定的问题
- 现在是只有4篇微博。但是我们熟悉的微博可能某个知名博主会有成千上万的微博,因此需要执行多次 hgetall 操作【在循环里访问查询数据库】。势必会造成很多次无效的网络IO请求
- 分页获取文章时,lrange 两端的文章还好。如果是中间的文章则会效率很慢,如果中间微博有1w条,如果将这1w条再拆分成10组,每组1k条,则会有一定的效率提升
-
-
-
set类型
Redis的Set结构与Java中的HashSet类似,可以看作是一个value为null的HashMap。
- 无序
- 不可重复
- 查找快
- 支持交并补查询
Set常用语法
语法 | 含义 |
---|---|
SADD key member | 向set中添加一个或多个元素 |
SREM key element | 移除set中的指定元素 |
SCARD key | 返回set中元素的个数 |
SISMEMBER key member | 判断一个元素是否存在于set中 |
SMEMBERS | 获取set中所有元素 |
SINTER key1 key2 | key1 和 key2 交集 |
SDIFF key1 key2 | key1 和 key2 差集集 |
Set集合,设置(和get相对)。把一些相关联的数据放到一起,集合中的元素是无序和不可重复的
有序:顺序很重要,变换一下顺序就是两个不同的list
- list:[1, 2, 3] 和 [1, 3, 2] 是两个不同的 list
无需:顺序不重要,变换一下顺序集合还是原来的集合
- set:[1, 2, 3] 和 [1, 3, 2] 是同一个集合
-
sadd、smembers、sismember
-
sadd
-
作用:把集合中的元素叫做 member
-
语法
-
SADD key member [member ...] FLUSHALL sadd key 1 2 3 1 2 3
-
-
时间复杂度:O(1)
-
返回值:本次操作成功添加了多少个元素
-
-
smembers
-
作用:获取set集合中全部元素,元素间的顺序无序
-
语法
-
SMEMBERS key smembers key
-
-
时间复杂度:O(N),N:元素个数
-
-
sismember
-
作用:判断元素是否在集合中
-
语法
-
SISMEMBER key member sismember key 3 sismember key 4
-
-
时间复杂度:O(1)
-
返回值:0:不在;1:在
-
-
-
spop和srandmember
-
spop
-
作用
- 无count:随机删一个
- count:写多少就删除多少直到空为止
-
语法
-
SPOP key [count] spop key spop key spop key spop key
-
-
时间复杂度:O(N)
-
返回值:删除的元素
-
-
srandmember
-
作用:随机获取一个元素
-
语法
-
SRANDMEMBER key [count] FLUSHALL sadd key 1 2 3 1 2 3 srandmember key srandmember key srandmember key srandmember key srandmember key srandmember key srandmember key 2 srandmember key 3
-
-
时间复杂度:O(N),N:返回的元素个数
-
返回值:返回的值【数组】
-
-
-
smove,srem,scard
-
smove
-
作用:把 member 从 source 上删除再插入到 destination 中
-
语法
-
SMOVE source destination member FLUSHALL sadd key 1 2 3 4 sadd key2 5 6 7 8 smove key key2 5 smembers key smembers key2 smove key key2 100
-
-
时间复杂度:O(1)
-
返回值:0:成功;1:失败
-
注意:移动一个不存在的数据也不会报错而是返回0
-
-
srem
-
作用:一次可以删除1个member也可以删除多个member
-
语法
-
SREM key member [member ...] srem key 1 srem key 2 3
-
-
时间复杂度:O(N)
-
返回值:删除元素个数
-
-
scard
-
作用:返回集合元素个数
-
语法
-
SCARD key
-
-
时间复杂度:O(1)
-
-
返回值:集合元素个数
-
-
集合间【交并差】
A:1, 2, 3, 4
B:3, 4, 5, 6
A ∩ B = 3 , 4 A \cap B = 3,4 A∩B=3,4
A ∪ B = 1 , 2 , 3 , 4 , 5 , 6 A \cup B = 1,2,3,4,5,6 A∪B=1,2,3,4,5,6
A ∖ B = 1 , 2 A \setminus B = 1,2 A∖B=1,2
B ∖ A = 5 , 6 B \setminus A = 5,6 B∖A=5,6
-
交集
-
sinter
-
作用:计算两个或多个集合交集
-
语法
-
SINTER key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sinter key1 key2
-
-
时间复杂度:O(N*M)【N:最小集合元素个数;M:最大集合元素个数】
-
返回值:交集结果
-
-
sinterstore
-
作用:计算好的交集结果放到destination这个key对应的集合中
-
语法
-
SINTERSTORE destination key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sinterstore key3 key1 key2 smembers key3
-
-
时间复杂度:O(N*M)【N:最小集合元素个数;M:最大集合元素个数】
-
返回值:交集的元素个数【要想知道交集的内容,直接按照集合的方式访问】
-
-
-
并集
-
sunion
-
作用:计算两个或多个集合并集
-
语法
-
SUNION key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sunion key1 key2
-
-
时间复杂度:O(N),N:总的元素个数
-
返回值:并集结果
-
-
sunionstore
-
作用:计算好的并集结果放到destination这个key对应的集合中
-
语法
-
SUNIONSTORE destination key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sunionstore key3 key1 key2
-
-
时间复杂度:O(N),N:总的元素个数
-
返回值:并集元素个数
-
-
-
差集
-
sdiff
-
作用:计算两个或多个集合差集
-
语法
-
SDIFF key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sdiff key1 key2
-
-
时间复杂度:
-
返回值:差集结果
-
-
sdiffstore
-
作用:计算好的差集结果放到destination这个key对应的集合中
-
语法
-
SDIFFSTORE destination key [key ...] FLUSHALL sadd key1 1 2 3 4 sadd key2 3 4 5 6 sdiff key1 key2 sdiffstore key3 key1 key2
-
-
时间复杂度:O(N)
-
返回值:差集元素个数
-
-
-
-
set内部编码
集合内部编码方式有两种
-
inset
-
intset(整数集合):当集合中的元素是整数并且元素个数小于 set-max-intset-entries配置(默认512)时,Redis会使用inset作为内部实现从而节约内存
-
-
hashtable
- 不满 inset 条件就会转为 hashtable
-
-
set应用场景
-
使用Set保存用户标签
-
给用户贴标签
-
sadd user:1:tags tag1 tag2 tag3...
-
-
给标签添用户
-
sadd tag1:users user1 user2 user3...
-
-
删除用户下标签
-
srem user:1:tags tag1 tag2 tag3...
-
-
删除标签下用户
-
srem tag1:users user1 user2 user3...
-
-
计算用户共同爱好
-
sinter user:1:tags user:2:tags
-
-
-
使用Set计算公共好友
基于“集合交集”
-
使用Set统计 UV
UV:user view,每个用户,访问服务器都会产生一个uv,但同一个用户多次访问不会使uv增加
PV:page view,每个用户访问该服务器,都会产生一个pv
-
zset类型
Redis的SortedSet是一个可排序的Set集合。与Java中的TreeSet类似,但底层数据结构差异很大。SortedSet中的每个元素都带有score属性,可以基于score属性对元素排序,底层是一个调表(SkipList)+Hash表
- 可排序
- 不可重复
- 查询快
SortedSet常用语法
语法 | 含义 |
---|---|
ZADD key score member | 添加一个或多个元素到SortedSet,如果已经存在则更新其score值 |
ZREM key member | 删除SortedSet中指定元素的score值 |
ZSCORE key member | 获取SortedSet中指定元素的score值 |
ZRANK key member | 获取SortedSet中指定元素排名【升序】 |
ZREVRANK key member | 获取SortedSet中指定元素排名【降序】 |
ZCOUNT key min max | 统计score值在给定范围内的所有元素的个数 |
ZINCRBY key increment member | 让SortedSet中指定元素自增,步长为指定的increment值 |
ZRANGE key min max | 按照score排序后,获取指定排名范围内的元素 |
ZRANGEBYSCORE key min max | 按照score排序后,获取指定score范围内的元素 |
ZINTER,ZUNION,ZDIFF | 交并差 |
List:有序【孙行者、行者孙、者行孙:不同的猴】
Set:无序,唯一【孙行者、行者孙、者行孙:同一只猴】
Zset:有序,唯一【所谓的有序性:升序、降序】
实际上 zset 内部是按照升序组织数据
-
zadd,zrange
-
zadd
-
作用:添加元素
-
语法
-
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member...] FLUSHALL zadd key 99 吕布 98 赵云 96 典韦 95 关羽 zrange key 0 -1 zrange key 0 -1 withscores zadd key 10 赵云 zrange key 0 -1 withscores
- 不加 NX | XX
- member不存在:新建
- member存在:更新
- NX:必须不存在,达到新建效果
- XX:必须要求存在,达到更新效果
- GT:新的分数比旧分数大才能更新,如果元素不存在则会达到“新建”
- LT:新的分数比旧分数小才能更新,如果元素不存在则会达到“新建”
- CH:changed缩写,返回值是更改【添加+更改】的元素总数,ZADD只会返回统计添加的个数
- INCR:相当于 ZINCRBY
如果修改的分数,则会重新排序
zadd key NX 94 张飞 zrange key 0 -1 withscores zadd key NX 92 张飞 zrange key 0 -1 withscores zadd key XX 92 张飞 zadd key XX 90 马超
使用
ch
影响返回结果zadd key ch 90 张飞
使用
incr
在原有基础上进行新增【类似于zincrby
修改效果】zadd key incr 4 张飞
- 不加 NX | XX
-
-
时间复杂度: O ( l o g N ∗ K ) O(log^{N} * K) O(logN∗K),K:添加 K 个元素
-
-
zrange
-
作用:查看有序集合中元素详情【类似 lrange 可以指定一对下标构成的区间】
-
语法
-
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES] zrange key 0 -1 rev withscores
-
-
时间复杂度: O ( l o g N + M ) O(log^{N}+M) O(logN+M)
-
-
-
zcard,zcount
-
zcard
-
作用:返回当前集合元素个数
-
语法
-
ZCARD key zrange key 0 -1 withscores zcard key
-
-
时间复杂度:O(1)
-
返回值:当前集合元素个数
-
-
zcount
-
作用:返回分数在 [min, max] 闭区间之间的元素个数,可通过 ( 排除
-
语法
-
ZCOUNT key min max zrange key 0 -1 withscores zcount key 94 96 zcount key (94 96 zcount key (94 (96
此处标识比较奇葩。闭区间:“”;开区间:“(”
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
先根据 min 找到一个元素下标
-
再根据 max 找到一个元素下标
下标相减求个数
-
-
返回值:满足条件的元素列表个数
-
扩展
-
zset在浮点数中,也支持 inf,-inf做为max,min
- -inf:负无穷大【不是无穷小区近0的意思,而是远离0】
- inf:正无穷大
zrange key 0 -1 withscores zcount key 0 99 zcount key -inf inf zrange -inf inf
-
-
-
-
zrange,zrevrange,zrangebyscore
-
zrange
-
作用:查看集合,按照分数升序排序
-
语法
-
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES] FLUSHALL zadd key 99 吕布 98 赵云 96 典韦 96 马超 95 关羽 94 张飞 zrange key 0 -1 withscores
-
-
时间复杂度: O ( l o g N + M ) O(log^{N} + M) O(logN+M)
-
-
zrevrange
-
作用:查看集合,按照分数降序排序
-
语法
-
ZREVRANGE key start stop [WITHSCORES] FLUSHALL zadd key 99 吕布 98 赵云 96 典韦 96 马超 95 关羽 94 张飞 zrevrange key 0 -1 withscores
-
-
时间复杂度: O ( l o g N + M ) O(log^{N} + M) O(logN+M)
-
返回值:
-
-
zrangebyscore-
作用:按照分数找元素,类似于 zcount【未来将弃用】
-
语法
-
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] FLUSHALL zadd key 99 吕布 98 赵云 96 典韦 96 马超 95 关羽 94 张飞 zrangebyscore key 94 96 withscores
-
-
时间复杂度: O ( l o g N + M ) O(log^{N} + M) O(logN+M)
-
返回值:
-
-
-
zpopmax
-
zpopmax
-
作用:删除并返回分数最高的 count 个元素
-
语法
-
ZPOPMAX key [count] FLUSHALL zadd key 99 吕布 96 赵云 96 典韦 96 马超 96 关羽 94 张飞 zpopmax key zpopmax key zpopmax key 2
删除 99 吕布之后出现了再删除一个元素发现有3个同分数96,结果会按照字典序降序排序删除
-
-
时间复杂度: O ( l o g N ∗ M ) O(log^{N}*M) O(logN∗M)。N:有序集合元素个数,M:count要删元素个数
-
这里的 l o g N log^N logN 我们可以通过一个变量记录尾删的位置,后续删除是不是可以达到 O(1) 呢?省区查找过程
-
-
返回值:被删除元素(member 和 score)
-
-
-
bzpopmax
有序集合其实也可以看为一个 “优先级队列”,有的时候也需要一个带有 “阻塞功能的” 的优先级队列。每个 key 都是一个有序集合
阻塞也是发生在有序集合为空的时候,阻塞到有其他客户端插入元素,也会有一个超时时间【s为单位,double类型】
-
bzpopmax
-
作用:删除最大值的阻塞版本【zpopmax有一个 count 参数,而阻塞版本没有这个参数】
-
语法
-
客户端1
-
BZPOPMAX key [key ...] timeout FLUSHALL bzpopmax key 600
-
-
客户端2
-
FLUSHALL zadd key 10 张三 20 李四 30 王五
-
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
-
-
zpopmin、bzpopmin
-
zpopmin
-
作用:删除有序集合中最小的元素
-
语法
-
ZPOPMIN key [count] FLUSHALL zadd key 10 张三 20 李四 30 王五 zpopmin key zpopmin key 2
-
-
时间复杂度: O ( l o g N ∗ M ) O(log^{N} * M) O(logN∗M)
-
返回值:被删除的元素集
-
-
bzpopmin
-
作用:删除有序集合中的最小元素阻塞版
-
语法
-
BZPOPMIN key [key ...] timeout
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
-
-
zrank、zrevrank、zscore
-
zrank
-
作用:得到元素在有序集合中的排名
-
语法
-
ZRANK key member [WITHSCORE] FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zrank key 李四
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
返回值:zrank得到的下标是升序计算的下标
-
-
zrevrank
-
作用:
-
语法
-
ZREVRANK key member [WITHSCORE] FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zrevrank key 李四
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
返回值:
-
-
zscore
-
作用:得到 member 的 score
-
语法
-
ZSCORE key member FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zscore key 张三
-
-
时间复杂度:O(1)
-
返回值:
-
-
-
zrem、zremrangebyrank、zremrangebyscore
-
zrem
-
作用:删除有序集合中的member元素
-
语法
-
ZREM key member [member ...] FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zrem key 张三 zrem key 李四 王五 小七
-
-
时间复杂度: O ( l o g N ∗ M ) O(log^{N} * M) O(logN∗M)。N:有序集合中元素个数,M:参数中member个数
-
返回值:删除成功的元素个数
-
-
zremrangebyrank
-
作用:根据下标,删除 [start, stop] 闭区间范围内的元素
-
语法
-
ZREMRANGEBYRANK key start stop FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zremrangebyrank key 1 2 zrange key 0 -1
-
-
时间复杂度: O ( l o g N + M ) O(log^{N} + M) O(logN+M)
-
返回值:成功删除元素个数
-
-
zremrangebyscore
-
作用:根据分数,删除指定 [min, max] 闭区间内元素
-
语法:
-
ZREMRANGEBYSCORE key min max FLUSHALL zadd key 10 张三 20 李四 30 王五 40 赵六 zremrangebyscore key 20 30 zrange key 0 -1
-
-
时间复杂度: O ( l o g N + M ) O(log^{N} + M) O(logN+M)
-
返回值:成功删除元素个数
-
-
-
zincrby
-
zincrby
-
作用:为指定元素的关联分数添加指定分数值【负数就减少】
-
语法
-
ZINCRBY key increment member FLUSHALL zadd key 10 zhangsan 20 lisi 30 wangwu 40 zhaoliu zincrby key 15 zhangsan zrange key 0 -1 withscores
-
-
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
-
返回值:增加元素后的分数
-
-
-
集合的交并差
之前集合的 sinter、sunion、sdiff 操作针对 zset 也有 zinter、zunion、zdiff
-
zinterstore
-
作用:将有序集合的计算结果保存到另一个集合中
-
语法
-
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>] FLUSHALL zadd key1 10 zhangsan 20 lisi 30 wangwu zadd key2 15 zhangsan 25 lisi 35 zhaoliu zinterstore key3 2 key1 key2 zrange key3 0 -1 withscores
默认就是分数相加
带有 WEIGHTS 权重的计算
zinterstore key3 2 key1 key2 weights 2 3 zrange key3 0 -1 withscores
带有 AGGREGATE 设计分数计算方式
zinterstore key3 2 key1 key2 aggregate max zrange key3 0 -1 withscores
-
-
时间复杂度: O ( N ∗ K ) + O ( l o g M ∗ M ) O(N * K) + O(log^{M} * M) O(N∗K)+O(logM∗M)
-
返回值:计算的集合中元素个数
-
-
-
zset内部编码
zset内部编码方式有两种:
- ziplist:个数少于 zset-max-ziplist-entries 配置(默认 128 个) + 每个元素值小于 zset-max-ziplist-value 配置(默认 64 字节) 。用 ziplist 节约内存
- skiplist:当 ziplist 条件不满足就会自动转为 skiplist
-
zset应用场景
有序集合最典型的应用场景就是排行榜系统。榜单的排名为度有多方面:时间、点赞量、浏览量。举一个按照点赞维护排行榜的例子:
对于内存的考虑:假设按照最火游戏之一王者荣耀计算
-
添加用户赞数
-
lihua发布的文章获得3个赞
-
zadd user:ranking:2023-10-31 3 lihua
-
-
后续又有人点赞
-
zincrby user:ranking:2023-10-31 1 lihua
-
-
-
取消点赞
-
lihua注销,平台删除时可以将用户从榜单中删除
-
zrem user:ranking:2023-10-31 lihua
-
-
-
查看点赞最多前10
-
zrevrangebyrank user:ranking:2023-10-31 0 9
-
-
展示用户信息及分数
用户名作为键后缀,将用户信息保存在哈希类型中。分数和排名可用
zscore
和zrank
获取-
hgetall user:info lihua zscore user:ranking:2023-10-31 lihua zrank user:ranking:2023-10-31 lihua
-
-
类型补充
-
stream
- List版本的 b l/r pop 的升级版消息队列,使用也更复杂
-
geospatial
- 存储经纬度
-
hyperloglog
-
应用场景只有一个:估算集合中元素个数【计数功能】
-
Hyperloglog不存储元素内容大能够记录“元素特征”,能够知道当前新增的元素是一个已经存在的元素还是一个崭新第一次出现的元素
-
-
bitmap
-
位图本质上还是一个集合,属于是Set类型针对整数的特殊化版本【节省空间】
-
Hyperloglog更省空间:存数字、字符串但不存元素内容只是计数效果。没有元素内容
-
bitmap:存储元素内容,有些业务场景还是需要bitmap存储的内容
-
-
-
bitfield
-
位域【类似于C语言中结构体在内存中的布局。主要是内存对齐】
-
struct type_my{ char a; int b; int c; }
-
-
redis官网用了一个 魔兽世界的梗 哥布林举例:补刀+金币的例子。补1个刀加50金币
-
初始化u32玩家金币
-
u32玩家补了一个刀,增加50金币
-
u32玩家买了件装备,扣掉999金币
-
查看u32玩家的数据
-
所以类似于C语言结构体
-
struct Player{ int count; int gold; }
-
-
-
渐进式遍历 scan
使用 scan
命令进行渐进式遍历从而防止 keys *
可能导致阻塞问题。每次 scan
时间复杂度 O(1)。需要完整地遍历完全部 key 需要多次运行 scan
作用:渐进式的方式遍历全部 key
语法
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
FLUSHALL
set k0 000
mset k1 111 k2 222 k3 333 k4 444 k5 555 k6 666 k7 777 k8 888 k9 999
scan 0
pattern:匹配模式
count:限制这一次遍历能够获取到多少个元素,默认10【此处的count只是给redis服务器一个提示/建议】
type:匹配的 key 类型
时间复杂度:O(1)
返回值:下一次 scan 的游标(cursor)以及本次 得到的 key
指定一下 count 参数
FLUSHALL
set k0 000
mset k1 111 k2 222 k3 333 k4 444 k5 555 k6 666 k7 777 k8 888 k9 999
scan 0 count 3
注意
数据库管理
切换数据库
关系型数据库中比如 MySQL 支持一个实例通过 字符串 控制多个数据库,而 Redis 则通过 数字 来控制16 个数据库。0:1号数据库,15:16号数据库。数据库中存储的数据即使重复也互不冲突,默认情况下使用的是0数据库
清除数据库
-
清除本数据库
-
FLUSHDB [ASYNC | SYNC]
-
时间复杂度:O(N)
-
-
清除全部数据库
-
FLUSHALL
-
获取数据库中key个数
DBSIZE
FLUSHALL
mset k0 000 k1 111 k2 222 k3 333 k4 444 k5 555 k6 666 k7 777 k8 888 k9 999
dbsize
Java客户端
MavenJedis快速入门
- Jedis和Lettuce:这两个主要是提供了Redis命令对应的API,方便操作Redis。而SpringDataRedis又针对这两种做了抽象和封装
- Jedis:语法和Redis类似,优点是使用快捷缺点是多线程环境下会出现不安全
- Lettuce:依靠opsForXxx进行操作Redis数据库,可解决多线程不安全情况
- Redisson:是在Redis基础上实现了分布式的可伸缩的Java数据结构。例如Map、Queue等。而且支持跨进程的同步机制:Lock、Semaphore等待,比较适合用来实现特殊功能需求
创建一个Maven项目,引入需要的依赖
Jedis官网
<!-- redis依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
一个redis小测试
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import java.util.Map;
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
// 1.建立连接
jedis = new Jedis("127.0.0.1", 6379);
// jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123456");
// 3.选择数据库
jedis.select(0);
}
@AfterEach
void close() {
if (jedis != null) {
jedis.close();
}
}
@Test
void testString() {
// 存数据
String result = jedis.set("name", "张三");
System.out.println("result = " + result);
// 取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
void testHash() {
jedis.hset("user:1", "name", "张三");
jedis.hset("user:1", "age", "13");
jedis.hset("user:1", "sex", "male");
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
}
打开客户端可以看到已经成功插入String和Hash类型的数据
由于经常的断开连接,建立连接会有消耗。所以以创建一个连接池
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
// 配置连接池
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接数
jedisPoolConfig.setMaxTotal(10);
// 最大空闲连接
jedisPoolConfig.setMaxIdle(10);
// 最小空闲连接
jedisPoolConfig.setMinIdle(0);
// 等待空闲时间[ms]
jedisPoolConfig.setMaxWaitMillis(100);
// 创建连接池对象,参数:连接池配置,服务端IP,服务端接口,超时时间,密码
jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 100, "123456");
}
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
SpringDataRedis客户端
SpringDataRedis官网简介
可以看到Redis的支持
创建一个Spring项目,添加如下依赖
yml配置如下
spring:
redis:
host: 127.0.0.1
port: 6379
password: Cxf@19307193096
lettuce:
pool:
max-active: 8 #最大连接数
max-idle: 8 #最大空闲连接
min-idle: 0 #最小空闲连接
max-wait: 1000ms #超时时间
测试代码如下
package app;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class TestRedisTemplate {
private RedisTemplate redisTemplate;
@Autowired
public TestRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Test
public void testString() {
// 写入一条 String 数据
redisTemplate.opsForValue().set("name", "张三");
// 获取一条 String 数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
会发现已经是乱码,可读性很差,因此需要用到Redis的序列化。那么问题出现在哪儿呢?我们顺着RedisTemplate
部分源码阅读一下
主要是 key和value 的序列化。redis中key一般用的都是字符串类型,因此使用的是String类型的序列化
程序会先通过 afterPropertiesSet
确定序列化方式
查看默认的 defaultSerializer
的属性如下所示,是一个 null
。所以会使用默认的 JDK序列化工具
我们再看 this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
方法
再看 (new SerializingConverter()
代码
再看 new DefaultSerializer()
代码
再看 serialize()
用的是 ObjectOutPutStream
序列化
上面了解了 JDK的序列化方式,SpringDataRedis集成了众多序列化工具,默认使用的是JDK序列化方式,对于普通对象而言使用则会出现一定乱码问题,SpringDataRedis更推荐使用大名鼎鼎的 Jackson
进行对对象序列化
自定义 Redis 的序列化器
package app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 RedisTemplate 对象
RedisTemplate redisTemplate = new RedisTemplate();
// 2.设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建 json 序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 3.设置 key 序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 4.设置 value 序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
// 5.返回 RedisTemplate
return redisTemplate;
}
}
测试结果如下所示
我们再测试一下对象的存储结果
再去redis数据库中查看
说明:对于普通字符串 “张三” 直接按照String类型存入到了redis中;而对于 User 对象则被 Jackson
序列化为了为了 json 类型的数据,为了能够方便通过 json 数据返回序列化出 User 对象还会多存入一条属性 "@class": "app.pojo.User"
。然而这样虽然反序列化方便了,但是数据量堆叠起来之后会给redis带了额外的内存开销
StringRedisTemplate 使用String序列化器
因此为了节省内存,一般并不会使用JSON序列化器,而是统一使用String序列化器,要求之存储String类型的key和value。当需要的时候在手动序列化或反序列化。
主要利用jackson的 ObjectMapper
类来实现手动的序列化和反序列化而不是通过Redis自带的JSON序列化工具
读写String
package app;
import app.pojo.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
@SpringBootTest
public class TestStringRedisTemplate {
private StringRedisTemplate stringRedisTemplate;
private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
public TestStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Test
public void testString() {
// 写入 String 数据
stringRedisTemplate.opsForValue().set("name", "张三");
// 读取 String 数据
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println(name);
}
@Test
public void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("李四", 24);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入 User 数据
stringRedisTemplate.opsForValue().set("user", json);
// 读取 User 数据
String jsonUser = stringRedisTemplate.opsForValue().get("user");
System.out.println("redis读取结果: " + jsonUser);
// 手动反序列化
user = mapper.readValue(jsonUser, User.class);
System.out.println("jsonUser反序列化: " + user);
}
}
会发现Redis在存储的时候已经消除掉多余的数据
读写Hash
在处理 Hash
类型的时候,语法hset
有些不同,更偏向于 Java 语法 put
@Test
public void testSaveHash(){
stringRedisTemplate.opsForHash().put("user:1", "name", "张三");
stringRedisTemplate.opsForHash().put("user:1", "age", "23");
// 获取单个字段
String name = (String) stringRedisTemplate.opsForHash().get("user:1", "name");
System.out.println(name);
// 获取全部
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:1");
System.out.println(entries);
}
总结
方案一
- 自定义
RedisTemplate
- 修改
RedisTemplate
序列化器为GenericJackson2JsonRedisSerializer
方案二
- 使用
StringRedisTemplate
- json序列化处理之后再写入redis
- 读取完redis之后再json反序列化成对象