在初期,已经讲述了Redis安装问题。现在正式进入Redis的入门阶段
一台机器运行应用程序、数据库服务器
现在大部分公司的产品都是这种单机架构。因为现在计算机硬件发展速度很快,哪怕只有一台主机,性能也很高的。可以支持几万级别的高并发和庞大的数据存储。
当业务进一步增长,用户量和数据量水涨船高。一台主机难以应付的时候就需要引入更多的主机和硬件资源
最主要的是CPU、内存、硬盘、网络
服务器每次收到一个请求,都是需要消耗上述的资源。如果同一时刻请求多了,某个硬件资源不够用【木桶效应】都可能会导致服务器处理请求时间变长甚至出错。一般的解决方案也是围绕着开源节流
- 开源:简单粗暴,增加更多的硬件资源【但一个主机能增加的硬件资源也是有限的】
- 一台主机扩展到了极限就会引入多台机器【可以通过一致性哈希算法分配和调整机器】,一旦引入多台主机就可以称为“分布式”
引入分布式是万不得已的方案。系统的复杂程度会大大提高,出现BUG的概率越高。加班概率&丢失年中奖的概率也随之提高
- 节流:软件上优化【通过性能测试,找到哪个环节出现了瓶颈再去对症下药】。但对技术水平要求高
传统的单主机架构既负责应用服务又要负责存储服务当业务量增长的时候就会出现第一次的性能瓶颈,此时可以通过基础的分布式:应用和存储分离进而分散流量压力,根据业务进行自定义服务器配置
应用和数据库分离,分别部署到不同主机上
如果还需要提升性能,可以考虑引入负载均衡,合理分配流量到对应的应用服务器
分布式解决的是初步的处理HTTP请求和数据库读写的性能瓶颈
如果应用服务器的CPU和内存资源吃完之后,还可以引入更多的应用服务器通过负载均衡器比较均匀的分配请求给应用服务器就可以有效解决第二次的性能瓶颈。集群中的某个主机宕机、其它的主机仍然可以承担服务提高整个系统的可用性
假设有1w个用户请求,有两个应用服务器就可以负载均衡模式就可以让每个应用服务器承担0.5w的访问量
和多线程有点像
负载均衡器就像公司的一个组的领导一样,要负责管理,把任务分配给每个组员。对于负载均衡器来说,有很多的 负载均衡 具体的算法,它对于请求量的承担能力远远超过服务器
负载均衡器是领导,分配任务
应用服务器是组员,执行任务
如果出现了请求量大到负载均衡器也扛不住压力的时候可以再引入更多的负载均衡器,甚至引入硬件资源(F5)进行更大的分流
负载均衡结局的是应用服务器处理大量HTTP请求问题
虽然增加应用服务器、确实可以处理更高的请求量。但是随之而来的存储服务器要承担的请求量也就更多
处理办法还是:开源+节流(门槛高,更复杂)。简单粗暴的增加一个存储服务器,将读写操作分离
一个数据库节点作为主节点负责写数据,其它N个数据库节点作为从节点负责读数据
但是数据库天然的问题就是:响应速度慢。解决方案就是进行数据区分引入缓存
数据库读写分离解决的是数据库读写性能瓶颈问题
把数据区分“冷热”、热点数据放到缓存中,缓存的访问速度比数据库很多
缓存服务器只是存放一小部分热点数据,采取二八原则【20%数据能够支持80%的访问量】
具体的 二八,三七,一九得看实际场景,略有差异
这里的缓存就用的Redis、但缺点就是内存小。从此可以看出缓存服务器扛住了大量的读请求负重前行,因此需要一个皮实的缓存服务器就可以进一步提高并发量
这里也需要考虑缓存中的数据同步问题,比如双十一商品会打折。那么主从数据库更新数据之后缓存也需要跟着修改
引入缓存解决的是数据库读的瓶颈问题
隐入新的问题:缓存的修改可能会出现数据同步一致性的问题
引入了分布式不仅要能够去应对高并发的请求量,同时也要能应对更大的数据量。因为依旧会存在一台服务器已经存不下数据的意外发生
比如短视频平台虽然一个服务器存储的数据量可以达到几十TB,但即使如此也会有大量用户数据存不下的情况发生
我们可以针对数据进一步的分析:分库分表
一个数据库服务器上有多个数据库(逻辑上的数据集合CREATE DATABASE db_name
)。现在就可以引入多个数据库服务器,每个数据库服务器存储一个或者一部分数据库
如果某个表特别大,比如订单表。大到一个服务器存不下,就需要对表进行拆分成多个服务器进行分开存储
数据库分库分表解决的是存储海量数据的问题
之前单个应用服务器做了很多业务,可能会导致服务器代码越来越复杂。后期为了维护方便,就可以把这样的一个复杂的服务器拆分成更多、功能更单一但是更小的服务器(微服务)
微服务本质上是解决的 “人” 的问题,但应用服务器复杂了,势必需要更多的人来维护了但也需要付出一定的代价
Redis的初心是用来作为一个“消息中间件”使用(消息队列),但当前很少使用Redis作为消息中间件(业界有更多更专业的消息中间件使用)
如果单机程序、直接通过变量存储数据的方式比Redis更优。但由于进程的隔离性,需要通过“网络”进行进程间通信
Redis就是基于网络把自己内存中的数据给被的进程甚至被的主机使用【分布式的前提准备】
通常互联网的热点数据也会遵守一个“二八原则”:20%的热点数据能满足80%的访问需求
打开官网就会发现它的特性
硬盘相当于对内存数据备份。当Redis重启就会在重启是加载硬盘中的备份数据使Redis的内存恢复到重启前的状态
一个Redis能够存储的数据有限(内存空间有限),引入多个主机。部署多个 Redis 节点,每个 Redis 存储数据的一部分
Redis数据在内存中,就比访问硬盘的速度快很多
Redis的核心功能都是比较简单的逻辑:操作内存的数据结构
从网络角度上,Redis使用了IO多路复用的方式(epoll)
Redis使用的是单线程模型
多线程提高效率的前提是:CPU密集型的任务。使用多个线程可以充分利用CPU多核资源
但是Redis的核心人物主要就是操作内存的数据结构,不会吃很多CPU,因此使用多线程还需要数据安全问题,锁竞争,开锁解锁的操作反而会拖累效率
Redis是用C语言开发的所以就快
MySQL也是C语言开发的,那么MySQL的慢和Redis的快也似乎没有必定的关联关系。如果用Python开发也会比C慢,但一般说Redis的快都是和MySQL作比较的
把Redis当作数据库使用:存全量数据
Redis 的多功能内存数据结构能够为需要低延迟和高吞吐量的实时应用程序构建数据基础设施
在线搜索引擎项目中构建的一些索引数据结构
把Redis当作缓存使用:存热点数据/会话
Redis 的速度使其成为缓存数据库查询、复杂计算、API 调用和会话状态的理想选择
当用户第一次登陆时,负载均衡器分配A服务器,此时BC服务器没有保存用户的会话sessionID。当用户再次访问的时候如果分配到了BC服务器处理该用户请求的话就会重新登陆
解决方案:
消息队列
流数据类型支持高速数据摄取、消息传递、事件源和通知
但业界更多的是用RabitMQ、Kafka、RocketMQ
Redis初衷是做消息队列,但是阴差阳错它的缓存功能大火被当作缓存用。它的消息队列功能后续也就停止发展,没有其它专业的消息队列功能强大
Redis最不能做的事情就是存储大规模的数据
redis中的命令不区分大小写
作用:通过正则查询当前服务器上匹配的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
matches hello
, hallo
and hxllo
h*llo
matches hllo
and heeeello
h[ae]llo
matches hello
and hallo,
but not hillo
h[^e]llo
matches hallo
, hbllo
, … but not hello
h[a-b]llo
matches hallo
and hbllo
时间复杂度:O(N)
注意
redis 是一个单线程服务器,keys * 执行的时间非常长,就使 redis 服务器被阻塞,此时其它请求的查询超时之后就会直接查询MySQL数据库,突然一大波请求过来,MySQL措手不及就容易挂掉
整个系统也就基本瘫痪了,要是没能及时发现及时恢复的话年终奖妥妥的就没啦,更严重工作也被一波带走
未来工作中会涉及到的几个环境
办公电脑难以支撑
办公环境、开发环境、测试环境、线下环境。外界用户无法访问到
线上环境则是 外界用户 能够访问到的,一但生产环境出问题,一定会对于用户的使用产生影响
EXISTS key [key ...]
exists hbllo
exists hallo
exists hbllo hallo
```![在这里插入图片描述](https://img-blog.csdnimg.cn/61fee7bb503d4b4ba171f6132f68a093.png)
redis 组织这些 key 是按照 哈希表 的方式来组织的
这里针对的是多个 key 来说,是非常有用的
比如上文中分两次判定 key 是否存在和一次判定两个 key 的存在。redis 是一个客户端《==》服务器结构的程序,客户端和服务器之间通过网络来进行通信,网络通信成本高效率低(封装复用)
DEL key [key ...]
del hallo
del hbllo hcllo hzllo
```![在这里插入图片描述](https://img-blog.csdnimg.cn/7d515b829ba3492ab69635a514c6c8fd.png)
> 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>
key 存活时间超出这个指定的时间,就会被自动删除
很多业务场景都有时间限制:手机验证码5分钟内有效,外卖的优惠券,基于 redis 实现 分布式锁(为了避免出现不能正确解锁的情况,通常都会在加锁的时候设置一下过期时间)
EXPIRE key seconds [NX | XX | GT | LT]
expire hzllo 10
expire habcdello 5
get habcdello
# 对于计算机来说:秒 是一个非常长的时间
PEXPIRE key milliseconds [NX | XX | GT | LT]
```![在这里插入图片描述](https://img-blog.csdnimg.cn/dad1350b542c415383ad867c021d49f2.png)
必须针对已经存在的 key
IP协议包头中就有一个字段TTL,它用转发次数衡量的
TTL key
expire hddllo 10
ttl hddllo
get hddllo
# 如果对时间有更高的精度,则可以使用 PTTL
PTTL key
```![在这里插入图片描述](https://img-blog.csdnimg.cn/d406f214d1cd4f67a5836e00c3931bf5.png)
redis 的 key 过期策略是怎么实现的?
一个 reids 中可能同时存在很多 key,这些 key 中可能有很大一部分有过期时间。此时 redis 服务器如何知道哪些 key 已经过期要被删除,哪些 key 还未过期呢?
redis过期策略主要分为两大类
定期删除
每次抽取一部分验证过期时间,保证这个抽取检查的过程足够快
这里对定期删除有明确的时间要求原因:因为 redis 是单线程程序,如果扫描过期 key 消耗时间过多就可能导致正常处理请求命令被阻塞(产生类似于 key * 效果)
惰性删除
虽然有了上述两种策略结合,但整体效果一般。仍然可能会有很多过期的 key 被残留,没有被及时清理
因此 redis 又提供了一些列的内存定期淘汰策略
定时器:在某个时间到达之后执行指定任务
优先级队列
实现原理:把要执行的任务放入优先级队列中,此时定时器中只要分配一个线程,让这个线程去检查队首元素是否过期
用小堆: 此时只需要每次扫描堆顶元素而不需要遍历所有 key
扫描线程检查队首元素是否过期时候也不能太频繁,因为会无缘无故消耗很多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);
}
}
如果有一个key设置的过期时间特别长,比如3000ms。则会转3圈过后放在第5个格子【600ms】,然后当蓝色指针转到第6个格子时会检查该链表上的任务时间是否到了,到了才执行
但Redis的定时器并未按照上述两种方案进行设计,初衷可能是不引入多线程。但上述两种方案是比较高效的实现方式,很多场景也会用到
Redis底层在实现上述数据结构的时候会在源码层面进行优化来达到 节省时间/空间 效果。
Redis内部有个 hash表 再进行 CRUD 操作可以保证 O(1) 复杂度但是背后的实现不一定是一个标准的 hash表,可能特定场景下使用别的数据结构但是仍然保证时间复杂度符合 O(1)
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 服务器实际上是单线程模型,保证了当前收到的这个请求是串行执行的
多个请求同时到达Redis服务器也是要在队列中排队,在等待Redis服务器一个一个的取出里面的命令再执行【微观上讲Redis服务器是串行执行多个命令的】
这里的快参照物是数据库(MySQL,Oracle,SQLServer等)
整体上来说Redis是键值对结构,key 固定就是字符串,value实际上会有多种类型
Redis 中的字符串按照二进制数据的方式存储(不会做任何编码转换)
文本字符串,xml,json,帧数,二进制数据(图片/音频/视频)
二进制数据提及可能会比较大,最大限制为512MB
一般来讲,Redis乱码概率很小
String字符串是Redis最简单的存储类型。
根据字符串格式不同,可以分为3类
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
就会冲突。解决方案是:多个单词之间用 : 分隔开,格式如下:
项目名:业务名:类型:id
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
FLUSHALL:相当于数据库的 DROP DATABASE
时间复杂度:O(1)
这里的 key value 不需要加上引号就表示字符串类型【如果要加上也是可以的(单双引号都可以)】
如果key不存在则新建键值对
如果key存在则覆盖旧的value,可能会更改原来的数据类型、原来的TTL也会失效
NX:如果 key 不存在,则设置(存在则返回NIL)
XX:如果 key 存在,则设置(不存在则返回NIL)
将两步操作并为一步
set key1 value1
expire value1 10
set key1 value1 ex 5
get
mset 和 mget
setnx、setex、psetex
incr,incrby和incrbyfloat
incr
作用:针对value+1
语法
时间复杂度:O(1)
返回值:+1 之后的值【++i】
注意
此时 key 对应的 value 必需为整数
incrby
incrbufloat
decr 和 decrby
其它字符串操作
append
getrange
作用:获取子串
语法
时间复杂度:O(N)
N:代表的是value长度,但实际情况依旧是可以当为1【几百个字节对计算机也是小问题 】
返回值:string类型的字串
注意
setrange
作用:替换字符串
从第几个字介开始,往后覆盖掉value个字符
语法
时间复杂度:O(N)
N:代表的是value长度,但实际情况依旧是可以当为1【几百个字节对计算机也是小问题 】
返回值:替换之后新字符串长度
注意
strlen
作用:获取字符串长度【单位是字节】
语法
STRLEN key
FLUSHALL
set key helloworld
strlen key
strlen key1
时间复杂度:O(1)
返回值:string的长度,如果key不存在则返回0
注意
string类型编码方式
字符串类型的内部编码有3中:
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
说明Redis存储小数本质上是存储字符串意味着:每次进行算术运算都需要把字符串转成小数进行运算再转会字符串保存
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;
}
}
Redis设计合理的key有助于区分数据对象,比如:
MySLQ库名:cux_om,供应商表:cux_om_vendors,对应的 Redis key:cov:6379:vendor_info: telephone:123456789
但是这样的key虽然完整,但是键名过长也会降低性能:cov:6379:vi:tel:123456789
计数(Counter)功能
记录视频播放次数
为什么使用Redis统计而不用MySQL?
因为用户数量多的时候,MySQL针对相应的视频执行一条 UPDATE 命令,尤其是短视频平台,一个用户不会只刷一个视频,因此MySQL会承受不了这样的压力
什么是异步写入?
写入统计数据库步骤不一定和Redis统计步骤一致,MySQL数据库慢一点但是一直在写来保证跟上Redis进度。当流量低的时候会能写多少数据就尽量写多少
实际开发过程中还要考虑很多:防作弊【此用户单个视频刷了成百上千遍】、按照不同维度统计【用户点进去就滑走】、避免单点登录问题【某台服务器挂掉后用户需要重复登陆】、数据持久化到底层数据源【不能服务器重启就丢失数据】
Session会话
Cookie:浏览器存储数据机制
Session:服务器存储数据的机制
左边:如果每个服务器只存储自己的会话信息不共享,当用户请求到不同服务器上就会可能出现不能处理的情况
右边:此时所有的会话信息都存储到Redis,多个服务器共享此Redis数据
手机验证码
很多应用出于安全考虑,安全登陆的时候让用户输入手机号再发送验证码短信,再让用户输入验证码从而验证是本人
为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率比如一分钟5次
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;
}
}
什么是业务
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
hexists
hdel
hkeys和hvals
hgetall和hmget
hgetall
hmget
上述 hkeys,hvals,hgetall 都是一次性获取全部。因此需要用渐进式遍历 hscan,运行一次遍历一小部分再运行再遍历一小部分,连续多次就可完成整个遍历过程
hlen、hsetnx、hincr、hincrby和hincrbyfloat
hlen
hsetnx
hincrby
hincrbyfloat
hash内部编码
哈希表编码方式主要有两种,ziplist和hashtable
哈希应用
作为缓存
关系型数据表保存用户信息
映射关系表示用户信息
相较于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;
}
JSON格式如果要想获取某个 field或者修改某个 field,就需要把整个 json 读出来,解析成对象,操作 field 之后才重写转成 json 字符串再写回去
数据库稀疏性对比
缓存方式对比
原生字符串类型:每个属性一个键
set user:1:name James
set user:1:age 23
set user:1:city Beijing
序列化字符串JSON格式
set user:1 经过序列化后的用户对象字符串
哈希类型
hmset user:1 name James age 23 city Beijing
列表(List)相当于数组或者顺序表【并非是一个简单数组二十一个更接近于双端队列deque】,两端可以插入(push)或者弹出(pop),还可以获取指定范围的元素列表
从两端插入/删除元素都是非常高效O(1)
rpush 和 rpop:栈
rpush 和 lpop:队列
列表中的元素是有序的【输出顺序按照存放顺序】
元素允许重复
因为当前的 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
lpushx,rpush,rpushx
lpushx
作用:在 key 存在时,将⼀个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回
语法
时间复杂度:插入一个元素:O(1);插入N个元素:O(N)
返回值:返回值是 list 长度
注意
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
lindex,linsert,llen
lindex
linsert
llen
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
count:要删除的元素个数【正数:从左往右;负数:从右往左】
element:要删除的元素
时间复杂度:O(N+M)
返回值:被删除元素个数
ltrim,lset
blpop和brpop
阻塞版本命令:blpop,brpop分别是lpop,rpop的阻塞版本
redis中的list也相当于阻塞队列一样,线程安全是通过单线程模型支持的,阻塞:则只支持“队列为空”的情况而不考虑“队列为满”的情况
如果 list 中存在元素:blpop,brpop和 lpop,rpop 作用完全相同
如果 list 为空:blpop和brpop就会产生阻塞,一直阻塞到队列不为空为止【但一般不提倡无休止的等】
队列空 | |
---|---|
lpop,rpop | nil |
blpop,brpop | 根据timeout阻塞一段时间,阻塞期间redis可以执行其它命令 |
这里的阻塞看似会产生一些耗时操作,但其实并不会对redis服务器产生负面影响
这两个阻塞命令主要用来满足 “消息队列” 这样需求
blpop和brpop都是可以同时去尝试获取多个 key 的列表的元素。命令中如果设置了多个键,它会从左至右进行遍历键,一旦有一个键对应的列表可以弹出元素,命令就返回哪个元素。
如果多个客户端同时多个键执行 bl/rpop,则最先执行命令的客户端会得到弹出的元素
blpop
brpop
作用:
语法
BRPOP key [key ...] timeout
时间复杂度:O(N),N:list元素个数
list内部编码
list列表内部编码方式也有两种
list应用场景
用 list 作为 “数组” 这样的结构来存储多个元素
消息队列
微博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
}
此方案存在一定的问题
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
无需:顺序不重要,变换一下顺序集合还是原来的集合
sadd、smembers、sismember
sadd
smembers
sismember
spop和srandmember
spop
srandmember
smove,srem,scard
smove
srem
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
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
sunionstore
差集
sdiff
sdiffstore
set内部编码
集合内部编码方式有两种
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增加
uv需要按照用户进行去重,可用set实现
PV:page view,每个用户访问该服务器,都会产生一个pv
Redis的SortedSet是一个可排序的Set集合。与Java中的TreeSet类似,但底层数据结构差异很大。SortedSet中的每个元素都带有score属性,可以基于score属性对元素排序,底层是一个调表(SkipList)+Hash表
因为SortedSet可排序特性,经常用来实现排行榜这样的功能
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 内部是按照升序组织数据
Zset为了有一个排序规则,给Zset中的member引入了一个属性分数(score),浮点类型。进行排序的时候就会按照此处的 分数 大小升/降序
分数相同:关羽张飞都是 97.8 分,会按照雨字符串本身字典徐来排序
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
如果修改的分数,则会重新排序
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 马超
zadd key XX 92 张飞
返回值是0代表添加了0个元素而不是修改失败
zadd key XX 90 马超
XX的原因导致添加了0条数据
使用 ch
影响返回结果
zadd key ch 90 张飞
使用 incr
在原有基础上进行新增【类似于 zincrby
修改效果】
zadd key incr 4 张飞
时间复杂度: O ( l o g N ∗ K ) O(log^{N} * K) O(logN∗K),K:添加 K 个元素
由于zset是有序结构,要求新增元素要放到合适的位置上(找位置)
zset内部数据结构本质是 跳表
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] 闭区间之间的元素个数,可通过 ( 排除
语法
时间复杂度: O ( l o g N ) O(log^{N}) O(logN)
先根据 min 找到一个元素下标
再根据 max 找到一个元素下标
下标相减求个数
返回值:满足条件的元素列表个数
扩展
zrange,zrevrange,zrangebyscore
zrange
zrevrange
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 个元素
语法
时间复杂度: 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) 呢?省区查找过程
但是redis目前没有这样做:redis源码中,针对有序集合,确实记录了尾部这样的特定位置,但实际上在删除的时候调用了一个 “通用的删除函数”【给定一个 member 值,进行查找。找到位置之后再删除】
返回值:被删除元素(member 和 score)
bzpopmax
有序集合其实也可以看为一个 “优先级队列”,有的时候也需要一个带有 “阻塞功能的” 的优先级队列。每个 key 都是一个有序集合
阻塞也是发生在有序集合为空的时候,阻塞到有其他客户端插入元素,也会有一个超时时间【s为单位,double类型】
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
zrevrank
zscore
zrem、zremrangebyrank、zremrangebyscore
zrem
zremrangebyrank
zremrangebyscore
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
后缀 store 可以将集合计算结果保存到另一个集合中
zset 的多集合运算多了个 numkeys 指定多少个 key 参与计算
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
numkeys:后续有多少个 key 参与计算
WEIGHTS:权重
AGGREGATE:对于不同分数但相同的 member 结合运算完之后的分数如何统计【默认求和】
默认就是分数相加
带有 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)
N:若干集合中里面集合元素个数最小的个数
K:K个集合求交集
M:排序结果集中M个元素
返回值:计算的集合中元素个数
zset内部编码
zset内部编码方式有两种:
zset应用场景
有序集合最典型的应用场景就是排行榜系统。榜单的排名为度有多方面:时间、点赞量、浏览量。举一个按照点赞维护排行榜的例子:
对于内存的考虑:假设按照最火游戏之一王者荣耀计算
userID:4字节
score:8字节
thousand千:kb
million百万:mb
billion十亿:G
一个用户12字节。假设有1亿用户就是就是12亿字节也就是1.2G
添加用户赞数
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
geospatial
hyperloglog
应用场景只有一个:估算集合中元素个数【计数功能】
假设Set有一个应用场景,统计服务器的UV(用户访问次数)。但问题在于:如果UV数据量非常大,Set就会消耗很多的内存空间
1亿UV,假设一个 userID 8字节,则一共 0.8G ≈ \approx ≈ 800MB。而Hyperloglogs则最多使用 12KB 达到此效果
bitmap
位图本质上还是一个集合,属于是Set类型针对整数的特殊化版本【节省空间】
Hyperloglog更省空间:存数字、字符串但不存元素内容只是计数效果。没有元素内容
存储元素的时候提取特征的过程是不可逆的【信息量丢失了】
bitmap:存储元素内容,有些业务场景还是需要bitmap存储的内容
bitfield
使用 scan
命令进行渐进式遍历从而防止 keys *
可能导致阻塞问题。每次 scan
时间复杂度 O(1)。需要完整地遍历完全部 key 需要多次运行 scan
首次运行
scan
会从 0 开始当
scan
返回的下次位置为 0 时,遍历结束
作用:渐进式的方式遍历全部 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服务器一个提示/建议】
此处的限制并非和MySQL的 limit不同,MySQL更精确
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
不需要管每次 cursor值多少,只需要根据redis服务器告知的cursor遍历即可
注意
- 除了
scan
外,Redis 面向哈希类型、集合类型、有序集合类型分别提供了 hscan、sscan,zscan 用法和scan
类似- 虽然渐进式遍历解决了阻塞问题,但是如果在遍历期间发生的(CRUD)是感知不到的,所以就可能导致遍历时 key 的 重复 或者 遗漏
- Redis服务器不会保留任何状态,因此遍历过程中可以任意中断
关系型数据库中比如 MySQL 支持一个实例通过 字符串 控制多个数据库,而 Redis 则通过 数字 来控制16 个数据库。0:1号数据库,15:16号数据库。数据库中存储的数据即使重复也互不冲突,默认情况下使用的是0数据库
虽然 Redis 支持多数据库,当需要两套完全隔离的数据库环境的时候建议用多个 Redis实例 而不是一个 Redis实例 创建出多个数据库这种做法
Redis实例对多数据库未提供太多特性,其次是无论多少个数据库Redis都是单线程模型,所以彼此之间还是需要排队等待。同时还会让开发、调试和运维工作变得复杂。因此推荐使用数据库0
清除本数据库
FLUSHDB [ASYNC | SYNC]
时间复杂度:O(N)
清除全部数据库
FLUSHALL
DBSIZE
FLUSHALL
mset k0 000 k1 111 k2 222 k3 333 k4 444 k5 555 k6 666 k7 777 k8 888 k9 999
dbsize
创建一个Maven项目,引入需要的依赖
Jedis官网
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>4.3.1version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiterartifactId>
<version>5.9.1version>
<scope>testscope>
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官网简介
可以看到Redis的支持
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
进行对对象序列化
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带了额外的内存开销
因此为了节省内存,一般并不会使用JSON序列化器,而是统一使用String序列化器,要求之存储String类型的key和value。当需要的时候在手动序列化或反序列化。
主要利用jackson的 ObjectMapper
类来实现手动的序列化和反序列化而不是通过Redis自带的JSON序列化工具
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);
}
}
在处理 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