【Redis】笔记(尚硅谷、黑马整合)

数据库 专栏收录该内容
16 篇文章 1 订阅

(推荐)Redis入门到精通【黑马程序员】https://www.bilibili.com/video/BV1CJ411m7Gc

第1章 NoSQL 简介

REmote Dictionary Server:是一种用C语言开发的开源的高性能键值对数据库。

1.1 技术的分类

  1. 解决功能性的问题

java、Servlet、Jsp、Tomcat、RDBMS、JDBC、Linux、Svn 等

  1. 解决扩展性的问题

Spring、 SpringMVC、SpringBoot、Hibernate、MyBatis等

  1. 解决性能的问题

NoSQL、java多线程、Nginx、MQ、ElasticSearch、Hadoop等

1.2 WEB1.0 及WEB2.0

  1. Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单节点服务器可以解决大部分问题.

  1. Web2.0时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据,加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战.

1.3 解决服务器CPU内存压力

思考: Session共享问题如何解决?

  • 方案一、存在Cookie中

此种方案需要将Session数据以Cookie的形式存在客户端,不安全,网络负担效率低

  • 方案二、存在文件服务器或者是数据库里

此种方案会导致大量的IO操作,效率低.

  • 方案三、Session复制

此种方案会导致每个服务器之间必须将Session广播到集群内的每个节点,Session数据会冗余,节点越多浪费越大,存在广播风暴问题.

  • 方案四、存在Redis中

目前来看,此种方案是最好的。将Session数据存在内存中,每台服务器都从内存中读取数据,速度快,结构还相对简单.

1.4 解决IO压力

将活跃的数据缓存到Redis中,客户端的请求先打到缓存中来获取对应的数据,如果能获取到,直接返回,不需要从MySQL中读取。如果缓存中没有,再从MySQL数据库中读取数据,将读取的数据返回并存一份到Redis中,方便下次读取.

扩展: 对于持久化的数据库来说,单个库单个表存在性能瓶颈,因此会通过水平切分、垂直切分、读取分离等技术提升性能,此种解决方案会破坏一定的业务逻辑,但是可以换取更高的性能.

1.5 NoSQL数据库概述

  1. NoSQL(NoSQL = Not Only SQL ),意即==“不仅仅是SQL”,泛指非关系型的数据库。==
  • NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
  1. NoSQL的特点
  • 不遵循SQL标准
  • 不支持ACID(原子性,一致性,持久性,隔离性)
  • 远超于SQL的性能。
  1. NoSQL的适用场景
  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的
  1. NoSQL的不适用场景
  • 需要事务支持
  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
  1. 建议: 用不着sql的和用了sql也不行的情况,请考虑用NoSql

1.6 常用的缓存数据库

  1. Memcached

  1. Redis

  1. mongoDB

  1. 列式数据库
  • 先看行式数据库

思考: 如下两条SQL的快慢

​ select * from users where id =3(快)

​ select avg(age) from users(慢)需要先查行年龄,再平均

  • 再看列式数据库

列式数据库对查平均快

  1. HBase

  1. Cassandra

  1. Neo4j

1.7 数据库排名

http://db-engines.com/en/ranking

第2章 Redis简介 及 安装

2.1 Redis是什么

简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis是一个开源的key-value存储系统

是完全开源免费的,用c语言编写的,是一个单线程,高性能的(key/value)内存数据库,基于内存运行并支持持久化的nosql数据库

和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。

与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

2.2 Redis的应用场景

  1. 配合关系型数据库做高速缓存
  • 高频次,热门访问的数据,降低数据库IO
  1. 由于其拥有持久化能力,利用其多样的数据结构存储特定的数据
  • 最新N个数据→通过List实现按自然事件排序的数据
  • 排行榜,TopN→利用zset(有序集合)
  • 时效性的数据,比如手机验证码→Expire过期
  • 计数器,秒杀→原子性,自增方法INCR、DECR
  • 去除大量数据中的重复数据→利用set集合
  • 构建队列→利用list集合
  • 发布订阅消息系统→pub/sub模式

2.3 Redis官网

  1. Redis官方网站 http://Redis.io

  2. Redis中文官方网站 http://www.Redis.net.cn

手册网址: http://doc.redisfans.com/

2.4 关于Redis版本

Redis官方没有提供对Windows环境的支持,是微软的开源小组开发了对Redis对Windows的支持.

2.5 安装步骤

1)    下载获得redis-3.2.5.tar.gz后将它放入我们的Linux目录/opt
wget http://......tar.gz
2)    解压命令:`tar -zxvf redis-3.2.5.tar.gz`

3)    解压完成后进入目录:`cd redis-3.2.5`

4)    在redis-3.2.5目录下执行make命令
编译:make
cd src
安装:make install 
或make PREFIX=/usr/local/redis install
# PREFIX= 这个关键字的作用是编译的时候用于指定程序存放的路径。比如我们现在就是指定了redis必须存放在/usr/local/redis目录。假设不添加该关键字Linux会将可执行文件存放在/usr/local/bin目录,

5)执行
默认生成的可执行文件在/usr/local/bin中
/usr/local/redis/bin/redis-server & ./redis.conf

创建软链接
ln -s 原始目录名 快速访问目录名
  1. 安装gcc与g++

运行Make命令时出现错误,提示 gcc:命令未找到 ,原因是因为当前Linux环境中并没有安装gcc 与 g++ 的环境

  • 能上网的情况:

yum install gcc

yum install gcc-c++

  1. 重新进入到Redis的目录中执行 make distclean后再执行make 命令.
Hint: It's a good idea to run 'make test' ;)
  1. 执行完make后,可跳过Redis test步骤,直接执行 make install
    INSTALL install
    INSTALL install
    INSTALL install
    INSTALL install
    INSTALL install
    make[1]: Leaving directory '/home/ftp/redis/redis-3.0.4/src'

2.6 查看默认安装目录 /usr/local/bin

放到这个目录下之后,可以在任何目录下访问。

  1. Redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何(服务启动起来后执行)

  2. Redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲

  3. Redis-check-dump:修复有问题的dump.rdb文件

  4. Redis-sentinel:Redis集群使用

  5. Redis-server:前台启动Redis

                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 3.0.4 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 26926
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                     

这是前台启动,我们要把他设置为后台启动。

  1. redis-cli:客户端,操作入口
 redis-server.exe 服务器启动命令
 redis-cli.exe 命令行客户端
 redis.windows.conf redis核心配置文件
 redis-benchmark.exe 性能测试工具
 redis-check-aof.exe AOF文件修复工具
 redis-check-dump.exe RDB文件检查工具( 快照持久化文件)

2.7 Redis的启动

  1. 默认前台方式启动
  • 直接执行redis-server 即可启动后不能操作当前命令窗口
  1. 推荐后台方式启动
  • 拷贝一份redis.conf配置文件到其他目录,例如根目录下的myredis目录 /myredis
  • 修改redis.conf文件中的一项配置 daemonize 将no 改为yes,代表后台启动
  • 设置自己的端口号
  • 执行配置文件进行启动 执行 redis-server /myredis/redis.conf
  • ps -ef | grep 进程名
  • kill -s 9 进程号
ouc-13   26926 22709  0 15:30 pts/4    00:00:00 redis-server *:6379
ouc-13   29372 28429  0 15:49 pts/3    00:00:00 grep --color=auto redis
端口号是6379
daemonize yes
#以守护进程方式启动,使用本启动方式, redis将以服务的形式存在,日志将不再打印到命令窗口中
port 6379
#设定当前服务启动端口号
dir "/自定义目录/redis/data"
#设定当前服务文件保存位置,包含日志文件、持久化文件(后面详细讲解)等
logfile "6***.log“
#设定日志文件名,便于查阅

启动多个redis

复制conf文件

改port

改pidfile

改logfile

改dir,每个不一样

集群配置:

cluster-enabled yes打开注释

cluster-config-file nodes-6379.conf打开注释并修改(可不改)

当你安装完成之后,你可以先执行 redis-server 让 Redis 启动起来,然后运行命令 redis-benchmark -n 100000 -q 来检测本地同时执行 10 万个请求时的性能:

redis-cli -h localhost -p 6379

2.9 关闭Redis服务

  1. 单实例关闭
  • 如果还未通过客户端访问,可直接 redis-cli shutdown
  • 如果已经进入客户端,直接 shutdown即可.
  1. 多实例关闭

l 指定端口关闭 redis-cli -p 端口号 shutdown

如何启动多个redis:

#默认配置启动
redis-server
redis-server –-port 6379
redis-server –-port 6380 ……
# 指定配置文件启动
redis-server redis.conf
redis-server redis-6379.conf
redis-server redis-6380.conf ……
redis-server conf/redis-6379.conf
redis-server config/redis-6380.conf ……
#默认连接
redis-cli
#连接指定服务器
redis-cli -h 127.0.0.1
redis-cli –port 6379
redis-cli -h 127.0.0.1 –port 6379

2.11 Redis的单线程+多路IO复用技术

nio解读:

  • 1.redis是基于内存的,内存的读写速度非常快(纯内存)。
  • 2.redis是单线程的,省去了很多上下文切换线程的时间(避免线程切换和竞态消耗)。
  • 3.redis使用epoll多路复用技术,可以处理并发的连接(非阻塞IO即NIO)。
  1. 多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

  2. Memcached 是 多线程 + 锁 ;Redis 是 单线程 + 多路IO复用.

1.阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情, 属于备胎做法.

2 非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会做其他事情, 属于专一做法.

3 IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们的区别是什么?
3.1 select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子。对应的编程模型就是:一个连接来了,就必须遍历所有已经注册的文件描述符,来找到那个需要处理信息的文件描述符,如果已经注册了几万个文件描述符,那会因为遍历这些已经注册的文件描述符,导致cpu爆炸。

3.2 poll大妈不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神
3.3 epoll大妈不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个==进宿舍楼的女生脸上贴上一个大字条,==上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你.

https://www.zhihu.com/question/28594409

上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属于阻塞状态

一个epoll场景:一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声“倒酒”(事件),你小跑过去给他倒一杯,然后随他去吧,突然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,可以干点别的玩玩手机。至于epoll与select,poll的区别在于后两者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。io多路复用大概就是指这几个醉汉共用一个服务员。

https://blog.csdn.net/happy_wu/article/details/80052617

接下来是异步IO的情况
你告诉女神我来了, 然后你就去王者荣耀了, 一直到女神下楼了, 发现找不见你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口. 此时属于逆袭做法

为什么 Redis 中要使用 I/O 多路复用这种技术呢?

首先,Redis 是单线程的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

要弄清问题先要知道问题的出现原因

由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I/O(read,write,accept等等),进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作。阻塞很正常, 接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端write(sockfd,but,size)发来数据。在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器仍不能处理,仍然阻塞在read(sockfd1,…)上。此时问题就出现了,不能及时处理另一个客户的服务,肿么办?I/O多路复用来解决!

继续上面的问题,有多个客户连接,socket-fd1、sockfd2、sockfd3…sockfdn同时监听这n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后就调用read读取收到消息的sockfd,然后又循环回select阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息。

Q:
那这样子,在读取socket1的数据时,如果其它socket有数据来,那么也要等到socket1读取完了才能继续读取其它socket的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程I/O有什么区别呢?

A:
1.CPU本来就是线性的,不论什么都需要顺序处理,并行只能是多核CPU。

2.I/O多路复用本来就是用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题,跟多线程没关系。

3.跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源。而I/O多路复用不需要切换线/进程,效率相对较高,特别是对高并发的应用nginx就是用I/O多路复用,故而性能极佳。但多线程编程逻辑和处理上比I/O多路复用简单,而I/O多路复用处理起来较为复杂。

理解IO多路复用

什么是I/O 多路复用

关于I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。结合下图可以清晰地理解I/O多路复用。

img

select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。

Reactor(反应器模式)

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

  • 用户线程监视一个socket,socket把监视信息注册到内核缓存recvBuf,等待数据到达,(用户线程不阻塞)
  • 数据到达之后,内核发送socket刻度信息,用户线程发送read请求去从recvBuf中读数据
  • read完成

多路分离函数select

如上图所示,用户线程发起请求的时候,首先会将需要进行IO操作的socket添加到select中,这时阻塞等待select函数返回。当数据到达时,select被激活,select函数返回,此时用户线程才正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

用户线程使用select函数的伪代码描述为:

{
   select(socket);//将需要进行IO操作的socket加入到select中

   while(1) {
       sockets = select();//获取被激活的socket
       for(socket in sockets) {
           if(can_read(socket)) {//拿被激活的socket去读
               read(socket, buffer);//从socket中把数据库读到buffer中
               process(buffer);//处理缓存中的数据
           }
       }
   }
}

其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。

然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

IO多路复用模型使用了Reactor设计模式实现了这一机制。

EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。

img

如上图,I/O多路复用模型使用了Reactor设计模式实现了这一机制。通过Reactor的方式,可以将用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起I/O请求时,数据已经到达了,用户线程一定不会被阻塞。

void UserEventHandler::handle_event() {

    if(can_read(socket)) {
        read(socket, buffer);
        process(buffer);
    }
}



{
    Reactor.register(new UserEventHandler(socket));
}

//用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。

Reactor::handle_events() {
    while(1) {
        sockets = select();
        for(socket in sockets) {
            get_event_handler(socket).handle_event();
        }
    }
}

//事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。

IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。

https://blog.csdn.net/happy_wu/article/details/80052617

总结

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

IO多路复用总结:

redis 采用网络IO多路复用技术,来保证在多连接的时候系统的高吞吐量

多路-指的是多个socket网络连接,复用-指的是复用一个线程

多路复用主要有三种技术:select,poll,epoll。epoll是最新的、也是目前最好的多路复用技术。

采用多路I/O复用技术:其一,可以让单个线程高效处理多个连接请求(尽量减少网络IO的时间消耗)。其二,Redis在内存中操作数据的速度非常快(内存里的操作不会成为这里的性能瓶颈)。主要以上两点造就了Redis具有很高的吞吐量。

二、为什么Redis是单线程的

2.1.官方答案

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

redis 核心就是 如果我的数据全都在内存里,我单线程的去操作 就是效率最高的,为什么呢,因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。

redis 用 单个CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案。

2.3.详细原因

1)不需要各种锁的性能消耗

Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁、释放锁操作,没有因为可能出现死锁而导致的性能消耗。

3)CPU消耗

采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。

但是如果CPU成为Redis瓶颈,或者不想让服务器其他CPU核闲置,那怎么办?

可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis

# 清除屏幕中的信息
clear

# 退出客户端
quit
exit
<ESC>

I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。

首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

阻塞式的 I/O 模型并不能满足这里的需求,我们需要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli),这里涉及的就是 I/O 多路复用模型了。
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)

第3章 Redis的五大数据类型

3.1 String

SDS 与 C 字符串的区别

为什么不考虑直接使用 C 语言的字符串呢?因为 C 语言这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。我们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 '\0'(下图就展示了 C 语言中值为 “Redis” 的一个字符数组)

C语言这样简单的数据结构可能会造成以下一些问题:

  • 获取字符串长度为 O(N) 级别的操作 → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组;
  • 不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题;
  • C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 '\0' 可能会被判定为提前结束的字符串而识别不了;

我们以追加字符串的操作举例,Redis 源码如下:

/* 在字符串s之后以二进制安全的的方式添加t字符串 
 * 调用之后,老的sds不再有效,所有的引用被新指针替换 */
sds sdscatlen(sds s, const void *t, size_t len) {
    // 获取原字符串的长度
    size_t curlen = sdslen(s);
  
    // 按需调整空间,如果容量不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
    s = sdsMakeRoomFor(s,len);    // 如果有重新分配内存,已经顺便将原来的s内容迁移好了
    if (s == NULL) return NULL;   // 系统内存不足,分配失败
    memcpy(s+curlen, t, len);     // 追加目标字符串到字节数组中
    sdssetlen(s, curlen+len);     // 设置追加后的长度
    s[curlen+len] = '\0';         // 让字符串以 \0 结尾,便于调试打印
    return s;
}

3.2 hash

hash相当于Java的HashMap,含义不再多解释

底层:哈希表

常用方法:

hdel key field1 [field2]

hmset key field1 value1 field2 value2...#设置多个
hmget key field1 field2...#获取多个
hlen key #字段数量
hexists key field #查是否存在指定字段

hkeys key
hvals key

hincrby key field increment
hincrbyfloat key field increment
例子:
127.0.0.1:6379> hset user id 01
(integer) 0
127.0.0.1:6379> hset user name lisi
(integer) 0
127.0.0.1:6379> hset user class 1
(integer) 1
# 即hset可以多句分开设置,不会覆盖、

127.0.0.1:6379> hgetall user
1) "id"
2) "01"
3) "name"
4) "lisi"
5) "class"
6) "1"

127.0.0.1:6379> hget user name
"lisi"
127.0.0.1:6379> hdel user class
(integer) 1
127.0.0.1:6379> hgetall user
1) "id"
2) "01"
3) "name"
4) "lisi"

127.0.0.1:6379> hmget user id name
1) "01"
2) "lisi"
127.0.0.1:6379> hexists user id
(integer) 1
127.0.0.1:6379> hexists user age
(integer) 0
127.0.0.1:6379> hkeys user
1) "id"
2) "name"

使用场景示例

hash类型应用场景:

淘宝购物车涉及与实现

业务分析:

  • 仅分析购物车的redis存储模型。添加、浏览、更改数量、删除、清空
  • 购物车于数据库间持久化同步(不讨论)
  • 购物车于订单间关系(不讨论)
    提交购物车:读取数据生成订单
    商家临时价格调整:隶属于订单级别
  • 未登录用户购物车信息存储(不讨论)
    cookie存储

解决方案

  • 以客户id作为key,每位客户创建一个hash存储结构存储对应的购物车信息
  • 将商品编号作为field,购买数量作为value进行存储
  • 添加商品:追加全新的field与value
  • 浏览:遍历hash
  • 更改数量:自增/自减,设置value值
  • 删除商品:删除field
  • 清空:删除key
  • 此处仅讨论购物车中的模型设计
  • 购物车与数据库间持久化同步、购物车与订单间关系、未登录用户购物车信息存储不进行讨论

场景2:双11活动日,销售手机充值卡的商家对移动、联通、电信的30元、 50元、 100元商品推出抢购活动,每种商品抢购上限1000张

解决方案

  • 以商家id作为key,3个key
  • 将参与抢购的商品id作为field
  • 将参与抢购的商品数量作为对应的value
  • 抢购时使用降值的方式控制产品数量
  • 实际业务中还有超卖等实际问题,这里不做讨论

扩缩容的条件

正常情况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容

当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave

3.3 List

LinkedList

业务场景:

微信朋友圈点赞,要求按照点赞顺序显示点赞好友信息
如果取消点赞,移除对应好友信息

# 移除指定数据,中间拿数据,但其实是从左面拿指定value,拿完count个value后结束
lrem key count value

list操作注意事项:

  • list中保存的数据都是string类型的,数据总容量是有限的,最多2^32 - 1 个元素 (4294967295)。
  • list具有索引的概念,但是操作数据时通常以队列的形式进行入队出队操作,或以栈的形式进行入栈出栈操作
  • 获取全部数据操作结束索引设置为-1
  • list可以对数据进行分页操作,通常第一页的信息来自于list,第2页及更多的信息通过数据库的形式加载

场景2:

twitter、新浪微博、腾讯微博中个人用户的关注列表需要按照用户的关注顺序进行展示,粉丝列表需要将最近关注的粉丝列在前面

新闻、资讯类网站如何将最新的新闻或资讯按照发生的时间顺序展示?
企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出?

解决方案

  • 依赖list的数据具有顺序的特征对信息进行管理
  • 使用队列模型解决多路信息汇总合并的问题
  • 使用栈模型解决最新消息的问题

tips:redis 应用于最新消息展示

// `adlist.h/listNode` 源码
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 adlist.h/list 结构来持有链表的话,操作起来会更加方便:

3.4 Set

缺点: 用户ID数据冗余

  • 第三种方案: 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

扩展:

srandmember key [count] #随机获取集合中指定数量的数据
spop key [count] #随机获取集合中的某个数据并将该数据移除集合
# 求两个集合的交、并、差集
sinter key1 [key2]
sunion key1 [key2]
sdiff key1 [key2]

# 求两个集合的交、并、差集并存储到指定集合中
sinterstore destination key1 [key2]
sunionstore destination key1 [key2]
sdiffstore destination key1 [key2]

# 将指定数据从原始集合中移动到目标集合中
smove source destination member

场景:

每位用户首次使用今日头条时会设置3项爱好的内容,但是后期为了增加用户的活跃度、兴趣点,必须让用户对其他信息类别逐渐产生兴趣,增加客户留存度,如何实现?

业务分析

  • 系统分析出各个分类的最新或最热点信息条目并组织成set集合
  • 随机挑选其中部分信息
  • 配合用户关注信息分类中的热点信息组织成展示的全信息集合

redis 应用于随机推荐类信息检索,例如热点歌单推荐,热点新闻推荐,热卖旅游线路,应用APP推荐,大V推荐等

场景2:

脉脉为了促进用户间的交流,保障业务成单率的提升,需要让每位用户拥有大量的好友,事实上职场新人不具有更多的职场好友,如何快速为用户积累更多的好友?
新浪微博为了增加用户热度,提高用户留存性,需要微博用户在关注更多的人,以此获得更多的信息或热门话题,如何提高用户关注他人的总量?
QQ新用户入网年龄越来越低,这些用户的朋友圈交际圈非常小,往往集中在一所学校甚至一个班级中,如何帮助用户快速积累好友用户带来更多的活跃度?
微信公众号是微信信息流通的渠道之一,增加用户关注的公众号成为提高用户活跃度的一种方式,如何帮助用户积累更多关注的公众号?
美团外卖为了提升成单量,必须帮助用户挖掘美食需求,如何推荐给用户最适合自己的美食?

Tips 9:
 redis 应用于同类信息的关联搜索,二度关联搜索,深度关联搜索
 显示共同关注(一度)
 显示共同好友(一度)
 由用户A出发,获取到好友用户B的好友信息列表(一度)
 由用户A出发,获取到好友用户B的购物清单列表(二度)
 由用户A出发,获取到好友用户B的游戏充值列表(二度)

set 类型数据操作的注意事项

  • set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份
  • set 虽然与hash的存储结构相同,但是无法启用hash中存储值的空间

场景3:

集团公司共具有12000名员工,内部OA系统中具有700多个角色, 3000多个业务操作, 23000多种数据,每位员工具有一个或多个角色,如何快速进行业务操作的权限校验?

场景4:

公司对旗下新的网站做推广,统计网站的PV(访问量) ,UV(独立访客) ,IP(独立IP)。
PV:网站被访问次数,可通过刷新页面提高访问量
UV:网站被不同用户访问的次数,可通过cookie统计访问量,相同用户切换IP地址, UV不变
IP:网站被不同IP地址访问的总次数,可通过IP地址统计访问量,相同IP不同用户访问, IP不变

解决方案
 利用set集合的数据去重特征,记录各种访问数据
 建立string类型数据,利用incr统计日访问量( PV)
 建立set模型,记录不同cookie数量( UV)
 建立set模型,记录不同IP数量( IP)

tips:redis 应用于同类型数据的快速去重

场景5:

黑名单
资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密进行出售。例如第三方火车票、机票、酒店刷票代购软件,电商刷评论、刷好评。
同时爬虫带来的伪流量也会给经营者带来错觉,产生错误的决策,有效避免网站被爬虫反复爬取成为每个网站都要考虑的基本问题。在基于技术层面区分出爬虫用户后,需要将此类用户进行有效的屏蔽,这就是黑名单的典型应用。
ps:不是说爬虫一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。

白名单
对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体,
依赖白名单做更为苛刻的访问验证。

解决方案
 基于经营战略设定问题用户发现、鉴别规则
 周期性更新满足规则的用户黑名单,加入set集合
 用户行为信息达到后与黑名单进行比对,确认行为去向
 黑名单过滤IP地址:应用于开放游客访问权限的信息源
 黑名单过滤设备信息:应用于限定访问设备的信息源
 黑名单过滤用户:应用于基于访问权限的信息源

tips:redis 应用于基于黑名单与白名单设定的服务控制

3.5 zset (sorted set)

它的

score不是数据。

  1. Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score) ,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

  2. 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

# 获取集合数据总量
zcard key
zcount key min max

# 集合交、并操作
zinterstore destination numkeys key [key ...]
zunionstore destination numkeys key [key ...]

主要应用:排行榜

  1. 思考: 如何利用zset实现一个文章访问量的排行榜?
语法:zadd <key>  <score1> <value1>
127.0.0.1:6379> zadd test 94 zs
(integer) 1
127.0.0.1:6379> zadd test 100 ls
(integer) 1
127.0.0.1:6379> zadd test 60 ww
(integer) 1
127.0.0.1:6379> zadd test 47 zl
(integer) 1
127.0.0.1:6379> zrange test 0 -1
1) "zl"
2) "ww"
3) "zs"
4) "ls"
127.0.0.1:6379> zrange test 0 -1 withscores
1) "zl"
2) "47"
3) "ww"
4) "60"
5) "zs"
6) "94"
7) "ls"
8) "100"
  • min与max用于限定搜索查询的条件
  • start与stop用于限定查询范围,作用于索引,表示开始和结束索引
  • offset与count用于限定查询范围,作用于查询结果,表示开始位置和数据总量

场景1:

票选广东十大杰出青年,各类综艺选秀海选投票
各类资源网站TOP10(电影,歌曲,文档,电商,游戏等)
聊天室活跃度统计
游戏好友亲密度
业务分析
 为所有参与排名的资源建立排序依据

 获取数据对应的索引(排名)
zrank key member
zrevrank key member
 score值获取与修改
zscore key member
zincrby key increment member

 redis 应用于计数器组合排序功能对应的排名

注意事项:

score保存的数据存储空间是64位,如果是整数范围是-9007199254740992~9007199254740992
 score保存的数据也可以是一个双精度的double值,基于双精度浮点数的特征,可能会丢失精度,使用时候要慎重
 sorted_set 底层存储还是基于set结构的,因此数据不能重复,如果重复添加相同的数据, score值将被反复覆盖,保留最后一次修改的结果

场景2:

基础服务+增值服务类网站会设定各位会员的试用,让用户充分体验会员优势。例如观影试用VIP、游戏VIP体验、云盘下载体验VIP、数据查看体验VIP。当VIP体验到期后,如果有效管理此类信息。即便对于正式VIP用户也存在对应的管理方式。
网站会定期开启投票、讨论,限时进行,逾期作废。如何有效管理此类过期信息。

解决方案:

对于基于时间线限定的任务处理,将处理时间记录为score值,利用排序功能区分处理的先后顺序
 记录下一个要处理的时间,当到期后处理对应任务,移除redis中的记录,并记录下一个要处理的时间
 当新任务加入时,判定并更新当前下一个要处理的任务时间
 为提升sorted_set的性能,通常将任务根据特征存储成若干个sorted_set。例如1小时内, 1天内,周内,月内,季内,年度等,操作时逐级提升,将即将操作的若干个任务纳入到1小时内处理的队列中

time # 获取当前系统时间

tips: redis 应用于定时任务执行顺序管理或任务过期管理

场景3:

任务/消息权重设定应用
当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,如何实现任务权重管理。

解决方案:

对于带有权重的任务,优先处理权重高的任务,采用score记录权重即可多条件任务权重设定
如果权重条件过多时,需要对排序score值进行处理,保障score值能够兼容2条件或者多条件,例如外贸订单优先于国内订单,总裁订单优先于员工订单,经理订单优先于员工订单
 因score长度受限,需要对数据进行截断处理,尤其是时间设置为小时或分钟级即可(折算后)
 先设定订单类别,后设定订单发起角色类别,整体score长度必须是统一的,不足位补0。第一排序规则首位不得是0
 例如外贸101,国内102,经理004,员工008。
 员工下的外贸单score值为101008(优先)
 经理下的国内单score值为102004

第4章 Redis的相关配置

  1. 计量单位说明,大小写不敏感

  2. include:类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

  3. ip地址的绑定 bind

  • 默认情况bind=127.0.0.1,只能接受本机的访问请求
  • 不写的情况下,无限制接受任何ip地址的访问
  • 生产环境肯定要写你应用服务器的地址
  • 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的相应
  1. tcp-backlog
  • 可以理解是一个请求到达后至到接受进程处理前的队列.
  • backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列
  • 高并发环境tcp-backlog 设置值跟超时时限内的Redis吞吐量决定
  1. timeout

一个空闲的客户端维持多少秒会关闭,0为永不关闭。

  1. tcp keepalive

对访问客户端的一种心跳检测,每个n秒检测一次,官方推荐设置为60秒

  1. daemonize

是否为后台进程

  1. pidfile

存放pid文件的位置,每个实例会产生一个不同的pid文件

  1. log level

四个级别根据使用阶段来选择,生产环境选择notice 或者warning

  1. log level

日志文件名称

  1. syslog

是否将Redis日志输送到linux系统日志服务中

  1. syslog-ident

日志的标志

  1. syslog-facility

输出日志的设备

  1. database

设定库的数量 默认16

  1. security

在命令行中设置密码

config get requirepass
config set requirepass "123456"
config get requirepass
auth 123456
get kl
  1. maxclient

最大客户端连接数

  1. maxmemory

设置Redis可以使用的内存量。一旦到达内存使用上限,Redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。如果Redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,

那么Redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

  1. Maxmemory-policy
  • volatile-lru:使用LRU算法移除key,只对设置了过期时间的键
  • allkeys-lru:使用LRU算法移除key
  • volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
  • allkeys-random:移除随机的key
  • volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
  • noeviction:不进行移除。针对写操作,只是返回错误信息
  1. Maxmemory-samples

设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小。

一般设置3到7的数字,数值越小样本越不准确,但是性能消耗也越小。

第5章 Jedis客户端

可视化客户端:Redis Desktop Manager

Jedis jedis = new Jedis("localhost",6379);
jedis.set("name","lisi");//方法名和原来的命令一致
jedis.get("name");
jedis.close();
//http://xetorthio.github.io/jedis/
  1. pom依赖
Commons-pool-1.6.jar
Jedis-2.1.0.jar

maven为jedis,可选junit
  1. 使用Windows环境下Eclipse连接虚拟机中的Redis注意事项
  • 禁用Linux的防火墙:Linux(CentOS7)里执行命令 : systemctl stop firewalld.service
  • redis.conf中注释掉bind 127.0.0.1,然后 protect-mode no。
package com.itheima;

import org.junit.Test;
import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Map;

public class JedisTest {
    @Test
    public void testJedis(){
        //1.连接redis
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //2.操作redis
//        jedis.set("name","itheima");
        String name = jedis.get("name");
        System.out.println(name);
        //3.关闭连接
        jedis.close();
    }

    @Test
    public void testList(){
        //1.连接redis
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //2.操作redis
        jedis.lpush("list1","a","b","c");
        jedis.rpush("list1","x");

        List<String> list1 = jedis.lrange("list1", 0, -1);
        for(String s : list1){
            System.out.println(s);
        }

        System.out.println(jedis.llen("list1"));

        System.out.println();
        //3.关闭连接
        jedis.close();
    }

    @Test
    public void testHash(){
        //1.连接redis
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //2.操作redis

        jedis.hset("hash1","a1","b1");
        jedis.hset("hash1","a2","a2");
        jedis.hset("hash1","a3","b3");

        Map<String, String> hash1 = jedis.hgetAll("hash1");

        System.out.println(hash1);

        System.out.println(jedis.hlen("hash1"));

        System.out.println();
        //3.关闭连接
        jedis.close();
    }

}

① 设定一个服务方法,用于模拟实际业务调用的服务,内部采用打印模拟调用
② 在业务调用前服务调用控制单元,内部使用redis进行控制,参照之前的方案
③ 对调用超限使用异常进行控制,异常处理设定为打印提示信息
④ 主程序启动3个线程,分别表示3种不同用户的调用

package com.itheima;

import com.itheima.util.JedisUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisDataException;

public class Service {
    private String id;
    private int num;

    public Service(String id,int num){
        this.id = id;
        this.num = num;
    }
    //控制单元
    public void service(){
//        Jedis jedis = new Jedis("127.0.0.1",6379);
        Jedis jedis = JedisUtils.getJedis();
        String value = jedis.get("compid:"+id);
        //判断该值是否存在
        try{
            if(value == null){
                //不存在,创建该值
                jedis.setex("compid:"+id,5,Long.MAX_VALUE-num+"");
            }else{
                //存在,自增,调用业务
                Long val = jedis.incr("compid:"+id);
                business(id,num-(Long.MAX_VALUE-val));
            }
        }catch (JedisDataException e){
            System.out.println("使用已经到达次数上限,请升级会员级别");
            return;
        }finally{
            jedis.close();
        }
    }
    //业务操作
    public void business(String id,Long val){
        System.out.println("用户:"+id+" 业务操作执行第"+val+"次");
    }
}

class MyThread extends Thread{
    Service sc ;
    public MyThread(String id,int num){
        sc = new Service(id,num);
    }
    public void run(){
        while(true){
            sc.service();
            try {
                Thread.sleep(300L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Main{
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("初级用户",10);
        MyThread mt2 = new MyThread("高级用户",30);
        mt1.start();
        mt2.start();
    }
}
package com.itheima.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.ResourceBundle;

public class JedisUtils {
    private static JedisPool jp = null;
    private static String host = null;
    private static int port;
    private static int maxTotal;
    private static int maxIdle;

    static {//定义放到外面
        ResourceBundle rb = ResourceBundle.getBundle("redis");//redis.properties
        host = rb.getString("redis.host");
        port = Integer.parseInt(rb.getString("redis.port"));
        maxTotal = Integer.parseInt(rb.getString("redis.maxTotal"));
        maxIdle = Integer.parseInt(rb.getString("redis.maxIdle"));
        JedisPoolConfig jpc = new JedisPoolConfig();
        jpc.setMaxTotal(maxTotal);
        jpc.setMaxIdle(maxIdle);
        jp = new JedisPool(jpc,host,port);
    }

    public static Jedis getJedis(){
        return jp.getResource();
    }
    public static void main(String[] args){
        JedisUtils.getJedis();
    }
}
// 原方法//这种每次都要拿一个连接池,没有效率,所以放到static代码块中
public static Jedis getJedis() {
    JedisPoolConfig jpc=new JedisPoolConfig();
    jpc.setMaxTotal(30);
    jpc.setMaxIdle(10);
    String host="127.0.0.1";
    int port=6379;
    JedisPool jp=new JedisPool(jpc,host,port);
    return jp.getResource();
}

redis.properties

redis.host=127.0.0.1
redis.port=6379
redis.maxTotal=30
redis.maxIdle=10

第6章 Redis 事务

和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。

在Redis中,MULTI/EXEC/DISCARD/WATCH这四个命令是我们实现事务的基石。

6.1 Redis中事务的定义

Redis中事务的实现特征:

  • 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。
    • 上面的意思是说线程不会为其他请求服务而已,但是redis使用了多路复用技术使得可以有多个用户socket链接着
  • 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
  • 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句。
  • 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
  • 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。

https://mp.weixin.qq.com/s/Va0kU6JQ5DpXH7mGM79pHA

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

Redis事务的主要作用就是串联多个命令防止别的命令插队

基本操作:

multi # 开启事务
作用:设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中

exec # 执行事务
作用:设定事务的结束位置,同时执行事务。与multi成对出现,成对使用
注意:加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行

discard # 取消事务
作用:终止当前事务的定义,发生在multi之后, exec之前
  1. 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,至到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

  2. 组队的过程中可以通过discard来放弃组队。

事务的注意事项

  • 事务过程中,输入的语法错误:会造成取消事务,且整体事务的所有命令均不会执行。包括正确的指令也不执行
  • 事务过程中,输入语法无误,但不符合逻辑:例如对list进行incr操作。能够正确运行的命令会执行,运行错误的命令不会被执行。注意:已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。
redis事务三特性
  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题

  • 不保证原子性:Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

手动进行事务回滚
 记录操作过程中被影响的数据之前的状态
单数据: string
多数据: hash、 list、 set、 zset

 设置指令恢复所有的被修改的项
单数据:直接set(注意周边属性,例如时效)
多数据:修改对应值或整体克隆复制

业务场景

天猫双11热卖过程中,对已经售罄的货物追加补货, 4个业务员都有权限进行补货。补货的操作可能是一系列的操作,牵扯到多个连续操作,如何保障不会重复操作?

业务分析:

 多个客户端有可能同时操作同一组数据,并且该数据一旦被操作修改后,将不适用于继续操作
 在操作之前锁定要操作的数据,一旦发生变化,终止当前操作

Redis事务监视锁(超卖问题、分布式锁)

Redis事务监听机制:

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:

val = GET mykey val = val + 1 SET mykey $val

以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景–竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:

WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC

和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。

watch的变量发生变化了,那么事务将不会执行。例如多终端修改。

# 对 key 添加监视锁,在执行exec前如果key发生了变化,终止事务执行  # 得写在multi外
watch key1 [key2……]

# 取消对【所有】 key 的监视
unwatch

EXEC和DISCARD也有取消监视的效果:如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

Tips 18:redis 应用基于状态控制的批量任务执行

业务场景

双11如何避免最后一件商品不被多人同时购买? 【超卖问题】

业务分析

 使用watch监控一个key有没有改变已经不能解决问题,此处要监控的是具体数据
 虽然redis是单线程的,但是多个客户端对同一数据同时进行操作时,如何避免不被同时修改?

watch的值是不停在改变的,1件的时候一个人买到其他人就消掉?他是监控一个值变没变的,而不是监控其他人能不能改这个值的

思想:

  • 利用setnx设置一个锁对象,拿到该锁对象才能操作业务,操作完是否该锁
解决方案:

setnx lock-keyname value # 设置一个公共锁
#value值不重要,比如我们要锁name这个变量,那么就是setnx lock-name true
利用setnx命令的返回值特征,有值则返回设置失败,无值则返回设置成功
 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作
 对于返回设置失败的,不具有控制权,排队或等待
set num 10
setnx lock-num 1 # setnx只是简单形式,实际比这复杂,还得加过期时间+更新过期时间
incrby num -1 #库存-1
del lock-num # 操作完毕通过del操作释放锁
如果别的终端在这个过程中页想加锁,别的终端是加不上的。到时他得重新输入命令他再锁

上面方案的问题:某个用户拿到锁之后还没delete呢,却宕机了,那该key岂不是永远不delete了?解决方案是给key加个过期时间

setnx lock-name 1
假如此时停电宕机了,却还没执行delete释放锁。

解决方案:给锁加失效时间
setnx lock-name 1
expire lock-name 20 #20s


# 此外,对象时间长的业务,可以检查所的失效时间,一旦小于1个值,重新设置过期时间,给自己的业务续命

由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。

  • 例如:持有锁的操作最长执行时间127ms,最短执行时间7ms。
  • 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
  • 锁时间设定推荐:最大耗时×120%+平均网络延迟×110%
  • 如果业务最大耗时<<网络平均延迟,通常为2个数量级,取其中单个耗时较长即可

6.3 事务中的错误处理

  • 组队阶段: 语法错误,命令错了
  • 执行阶段:对象错了,对象类型不对。(语法没错)
  1. 组队阶段,如果某个命令出现了报告错误,执行时整个的所有队列会都会被取消。

  1. 执行阶段,如果某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

6.4 为什么要做成事务?

悲观锁(Pessimistic Lock), 别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set CAS机制实现事务的。

6.6 Redis事务 秒杀案例

  1. 解决计数器和人员记录的事务操作

  1. 秒杀并发模拟 ab工具

CentOS6 默认安装 ,CentOS7需要手动安装

  • 联网: yum install httpd-tools
  • 无网络: 进入cd /run/media/root/CentOS 7 x86_64/Packages ,顺序安装

apr-1.4.8-3.el7.x86_64.rpm、 apr-util-1.5.2-6.el7.x86_64.rpm、 httpd-tools-2.4.6-67.el7.centos.x86_64.rpm

  • ab –n 请求数 -c 并发数 -p 指定请求数据文件 -T “application/x-www-form-urlencoded” 测试的请求
  1. 超卖问题

  2. 请求超时问题

节省每次连接redis服务带来的消耗,把连接好的实例反复利用

连接池参数:

MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。

maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;

MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;

testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;

  1. 遗留问题
  • LUA脚本

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂

  • LUA脚本在Redis中的优势

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作

但是注意redis的lua脚本功能,只有在2.6以上的版本才可以使用。

利用lua脚本淘汰用户,解决超卖问题。

第7章 Redis 持久化

是否正在加载RDB文件内容
最后一次保存之后改变的键的个数
是否正在后台执行RDB保存任务
最后一次执行RDB保存任务的时间
最后一次执行RDB保存任务的状态
最后一次执行RDB保存任务消耗的时间
如果正在执行RDB保存任务,则为当前RDB任务已经消耗的时间,否则为一1
最后一次执行RDB保存任务消耗的内存
是否开启了AOF功能
是否正在后台执行AOF重写任务(重写在后续的章节介绍)
是否等待调度一次OF重写任务。如果触发了一次AOF重写,但是后台正在执行RDB保存任务时会将该状态置为1
最后一次执行AOF重写任务消耗的时间
如果正在执行AOF重写任务,则为当前该任务巳经消耗的时问,否则为一1
最后一次执行AOF重写任务的状态
最后一次执行AOF缓冲区写入的状态(服务端执行命令时会开辟一段内存空f司将命令放入其中,然后从该缓冲区中同步到文

怎么保证 redis 挂掉之后再重启数据可以进行恢复

Redis提供了2个不同形式的持久化方式 RDB 和 AOF

  • RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据
  • AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程

表现为:

7.1 RDB

原理是redis会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化,这个子线程的所有数据(变量。环境变量,程序程序计数器等)都和原进程一模一样,会先将数据写入到一个临时文件中,待持久化结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何的io操作,这就确保了极高的性能(存疑)

fork

Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用

如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。

创建快照的办法有如下几种:

  • BGSAVE命令: 客户端向Redis发送 BGSAVE命令 来创建一个快照。对于支持BGSAVE命令的平台来说(基本上所有平台支持,除了Windows平台),Redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求。
    • 这个子进程和原进程数据一模一样,会先将数据写入到一个临时文件中,待持久化结束了,再用这个临时文件替换上次持久化好的文件,
    • 整个过程中,主进程不进程任何的IO操作,这就确保了极高的性能
  • SAVE命令: 客户端还可以向Redis发送 SAVE命令 来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常用,我们通常只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。
  • save选项: 如果用户设置了save选项(一般会默认设置),比如 save 60 10000,那么从Redis最近一次创建快照之后开始算起,当“60秒之内有10000次写入”这个条件被满足时,Redis就会自动触发BGSAVE命令。
  • SHUTDOWN命令: 当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在SAVE命令执行完毕之后关闭服务器。
  • 一个Redis服务器连接到另一个Redis服务器: 当一个Redis服务器连接到另一个Redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令

7.1.1 save

save #手动执行一次保存操作,会阻塞
bgsave # redis会在后台异步进行快照操作,同时可以响应客户端的请求
shutdown # 如果没有开启aof,会触发持久化
执行flushall命令,但是里面是空的,无意义

# 在redis.conf中配置
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

客户端执行save命令,该命令强制redis执行快照,这时候redis处于阻塞状态,不会响应任何其他客户端发来的请求,直到RDB快照文件执行完毕,所以请慎用。

save指令操作配置:需要在conf文件中配置

  • dbfilename dump.rdb
    • 说明:设置持久化后文件名(本地数据库文件名),默认值为 dump.rdb
    • 经验:通常设置为dump-端口号.rdb
  • dir
    • 说明:设置存储.rdb、log等文件的路径。加载位置也是这个位置
    • 经验:通常设置成存储空间较大的目录中,目录名称data
    • 保存时间分3种
  • rdbcompression yes
    • 说明:设置存储至本地数据库时是否压缩数据,默认为 yes,采用 LZF 压缩
    • 经验:通常默认为开启状态,如果设置为no,可以节省 CPU 运行时间,但会使存储的文件变大(巨大)
  • rdbchecksum yes
    • 说明:设置是否进行RDB文件格式校验,该校验过程在写文件和读文件过程均进行
    • 经验:通常默认为开启状态,如果设置为no,可以节约读写性过程约10%时间消耗,但是存储一定的数据损坏风险

要点:

  • save一次之后,删除rdb文件,重新save一次,rdb文件又出现(恢复)了
  • 退出后再登录,还有数据

RDB工作原理:

如图,4个客户端分别输入指令,redis是单线程的,所以需要排序。

有个问题是如果save是第三个,save执行时间很长,所以后面的get需要等待很长时间。就会阻塞。所以线上不建议使用save指令,会很拖慢性能。

注意: save指令的执行会阻塞当前Redis服务器, 直到当前RDB过程完成为止, 有可能会造成长时间阻塞, 线上环境不建议使用。

其他

# 查看最近一次持久化时间:
info Persistence

# 查看存储文件位置
CONFIG GET dir

7.1.2 bgsave

bgsave # 后台save#手动启动后台保存操作,但不是立即执行  

bgsave-fork原理:

img

Redis的单线程的,那如果主线程去做这种耗时的IO同步操作时,Redis整体的性能会被拖垮的。

fork()它是一个系统调用,一般用它来创建一个和当前进程一模一样的子进程。当在程序中调用它时,系统为新的进程分配存储、资源,将原程序中的值也复制给他

fork()函数调用一次会返回两次,在父进程得到的返回值是子进程的pid,在子进程中得到的是0,出错则返回负数。

Redis的实现是通过fork()系统调用创建一个子进程。由这个子进程去负责执行这些耗时的IO操作,父子进程会共享内存,然后被共享的这块内存不可写,新的数据写入到新的内存文件中。

  1. 客户端执行bgsave命令,redis主进程收到指令并判断此时是否在执行bgrewriteaof(AOF文件重写过程,后续会讲解),如果此时正好在执行则bgsave直接返回,不fork子进程,如果没有执行bgrewriteaof重写AOF文件,则进入下一个阶段;
  2. 主进程调用fork方法创建子进程,在创建子进程过程中redis主进程阻塞,所以不能响应客户端请求
  3. 子进程创建完成以后,bgsave命令返回“Background saving started”,此时标志着redis可以响应客户端请求了
  4. 子进程根据主进程的内存副本创建临时快照文件,当快照文件完成以后对原快照文件进行替换;
  5. 子进程发送信号给redis主进程完成快照操作,主进程更新统计信息(info Persistence可查看),子进程退出;

Redis会通过系统调用fork()出一个子进程,父子进程是会共享内存的,父进程和子进程共享的这块内存就是在执行fork操作那个时刻的内存快照。由linux的copy on write机制将父子进程共享的这块内存标记为只读状态。

此时对子进程来说,它的任务就是将这块只读内存中的数据保存成RDB文件。

对父进程来说它是有可能收到写命令的,当父进程尝试往这个加了只读状态的内存地址写入数据时,就会触发保护异常,执行linux的 copy on write,也就是将原来内存对应的数据页复制出来一份后,然后对这个副本进行修改

注意: bgsave命令是针对save阻塞问题做的优化。 Redis内部所有涉及到RDB操作都采用bgsave的方式, save命令可以弃用

bgsave命令可以理解为background save即:“后台保存”。当执行bgsave命令时,redis会fork出一个子进程来执行快照生成操作,需要注意的redis是在fork子进程这个简短的时间redis是阻塞的(此段时间不会响应客户端请求,),当子进程创建完成以后redis响应客户端请求。其实redis自动快照也是使用bgsave来完成的。

bgsave相关配置:

dbfilename dump.rdb
dir
rdbcompression yes
rdbchecksum yes
stop-writes-on-bgsave-error yes
说明:后台存储过程中如果出现错误现象,是否停止保存操作
经验:通常默认为开启状态

但是save和bgsave都需要手动保存,难免疏忽,使用需要自动执行。

conf文件配置持久化条件:

# 在配置文件中设置
格式:save 时间段 key修改次数
作用:满足限定时间范围内key的变化数量达到指定数量即进行持久化。即时间片内变化大才自动保存,是快照的思想。

 位置:在conf文件中进行配置,把3个都关掉就把RDB持久化关掉了。或者save "" # 但是集群环境下RDB是关不掉的
save 900 1 # 900s内变化一个就保存
save 300 10
save 60 10000

配置文件中save原理

发送3条指令,每条都会返回个结果,通过结果判断这条指令算不算影响数量。不进行数据比对指的是两个set就都算

RDB三种保存方式对比:

方式save指令bgsave指令save配置
读写同步异步
阻塞客户端指令
额外内存消耗
启动新进程

RDB特殊启动形式:

  • 全量复制:在主从复制中详细讲解
  • 服务器运行过程中重启:debug reload
  • 关闭服务器时指定保存数据:shutdown、 save

RDB优点:

RDB是一个紧凑压缩的二进制文件, 存储效率较高

  • RDB内部存储的是redis在某个时间点的数据快照, 非常适合用于数据备份,全量复制等场景
  • RDB恢复数据的速度要比AOF很多

应用:服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复。

RGB缺点:

RDB方式无论是执行指令还是配置,无法做到实时持久化,具有较大的可能性丢失数据

  • bgsave指令每次运行要执行fork操作创建子进程, 要牺牲掉一些性能
  • Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
  1. 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。

  2. 备份是如何执行的

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

  1. 关于fork

在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”,一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

  1. RDB保存的文件名

在redis.conf中配置文件名称,默认为dump.rdb

  1. RDB文件的保存路径

默认为Redis启动时命令行所在的目录下,也可以修改dir

  1. RDB的保存策略

  2. 手动保存快照

save: 只管保存,其它不管,全部阻塞

bgsave:按照保存策略自动保存

  1. RDB的相关配置
  • stop-writes-on-bgsave-error yes

当Redis无法写入磁盘的话,直接关掉Redis的写操作

  • rdbcompression yes

进行rdb保存时,将文件压缩

  • rdbchecksum yes

在存储快照后,还可以让Redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能

  1. RDB的备份 与恢复

备份:先通过config get dir 查询rdb文件的目录 , 将*.rdb的文件拷贝到别的地方

恢复: 关闭Redis,把备份的文件拷贝到工作目录下,启动redis,备份数据会直接加载。

  1. RDB的优缺点

优点: 节省磁盘空间,恢复速度快.

缺点: 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改

7.2 AOF

RDB弊端:

存储数据量较大,效率较低
基于快照思想,每次读写都是全部数据,当数据量巨大时,效率非常低

  • 大数据量下的IO性能较低
  • 基于fork创建子进程,内存产生额外消耗
  • 宕机带来的数据丢失风险

解决思路:

不写全数据,仅记录部分数据
 降低区分数据是否改变的难度,改记录数据为记录操作过程
 对所有操作均进行记录,排除丢失数据的风险

什么是AOF:

  • AOF(append only file)持久化:以独立日志的方式记录每次(写)命令,重启时再重新执行AOF文件中命令达到恢复数据的目的。与RDB相比可以简单描述为改记录数据为记录数据产生的过程
  • AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式
  • 比如10条incre命令,就自动替换为了set lock 11

当redis存储非临时数据时,为了降低redis故障而引起的数据丢失,redis提供了AOF(Append Only File)持久化,从单词意思讲,将命令追加到文件。AOF可以将Redis执行的每一条写命令追加到磁盘文件(appendonly.aof)中,在redis启动时候优先选择从AOF文件恢复数据。由于每一次的写操作,redis都会记录到文件中,所以开启AOF持久化会对性能有一定的影响,但是大部分情况下这个影响是可以接受的,我们可以使用读写速率高的硬盘提高AOF性能。与RDB持久化相比,AOF持久化数据丢失更少,其消耗内存更少(RDB方式执行bgsve会有内存拷贝)

默认情况下,redis是关闭了AOF持久化,开启AOF通过配置appendonly为yes开启,我们修改配置文件或者在命令行直接使用config set修改,在用config rewrite同步到配置文件。通过客户端修改好处是不用重启redis,AOF持久化直接生效。

AOF刷新缓存区

AOF写数据过程:

当客户端发出一条指令给服务器时,服务器收到并没有马上记录,而是放到临时区域:刷新缓存区,缓存区是最终存成文件时用的。

AOF持久化策略(写数据)

AOF写数据遇到的问题:

AOF重写过程(AOF 重写缓冲区)

在执行 BGREWRITEAOF命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作

7.3 RDB+AOF混合持久化

开启混合持久化

混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后再将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件

AOF工作流程:

  • Always时,Set正常执行,另开一个子进程进行重写。
  • Sec时会先放到缓存区。

AOF重写流程:

AOF缓冲区同步文件策略, 由参数appendfsync控制
系统调用write和fsync说明:

 write操作会触发延迟写( delayed write) 机制, Linux在内核提供页缓冲区用来提高硬盘IO性能。 write操作在写入系统缓冲区后直接返回。 同步硬盘操作依赖于系统调度机制, 列如:缓冲区页空间写满或达到特定时间周期。 同步文件之前, 如果此时系统故障宕机, 缓冲区内数据将丢失。

 fsync针对单个文件操作( 比如AOF文件) , 做强制硬盘同步, fsync将阻塞知道写入硬盘完成后返回, 保证了数据持久化。

除了write、 fsync、 Linx还提供了sync、 fdatasync操作, 具体API说明参见:

  1. AOF文件故障备份

AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载

  1. AOF文件故障恢复

如遇到AOF文件损坏,可通过redis-check-aof --fix appendonly.aof 进行恢复

  1. Rewrite

l AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。

l Redis如何实现重写

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

7.3 RDB和AOF比较

持久化方式RDBAOF
占用存储空间小(数据级:压缩)大(指令级:重写)
存储速度
恢复速度
数据安全性同步时间间隔大同步时间间隔小
资源消耗高/重量级低/轻量级
启动优先级

AOF和RDB同时开启,redis听谁的?:官方建议 两种持久化机制同时开启,如果两个同时开启 优先使用aof

  • 官方推荐两个都启用。
  • 如果对数据不敏感,可以选单独用RDB
  • 不建议单独用 AOF,因为可能会出现Bug。
  • 如果只是做纯内存缓存,可以都不用
  • 双保险策略, 同时开启 RDB 和 AOF, 重启后, Redis优先使用 AOF 来恢复数据,降低丢失数据的量

对数据非常敏感, 建议使用默认的AOF持久化方案
 AOF持久化策略使用everysecond,每秒钟fsync一次。该策略redis仍可以保持很好的处理性能, 当出
现问题时,最多丢失0-1秒内的数据。
 注意:由于AOF文件存储体积较大,且恢复速度较慢
 数据呈现阶段有效性,建议使用RDB持久化方案
 数据可以良好的做到阶段内无丢失(该阶段是开发者或运维人员手工维护的),且恢复速度较快,阶段
点数据恢复通常采用RDB方案
 注意:利用RDB实现紧凑的数据持久化会使Redis降的很低,慎重总结:
 综合比对
 RDB与AOF的选择实际上是在做一种权衡,每种都有利有弊
 如不能承受数分钟以内的数据丢失,对业务数据非常敏感, 选用AOF
 如能承受数分钟以内的数据丢失, 且追求大数据集的恢复速度, 选用RDB
 灾难恢复选用RDB

开机加载持久化步骤

优先加载aof文件

服务器配置

# -----服务器端设置-----
daemonize yes|no # 设置服务器以守护进程的方式运行

bind 127.0.0.1 # 绑定主机地址,如果不绑别人也能访问

port 6379 # 设置服务器端口号
databases 16  # 设置数据库数量


# -----日志配置-----
loglevel debug|verbose|notice|warning #  设置服务器以指定日志记录级别
logfile 端口号.log  #  日志记录文件名

#  -----客户端配置-----
maxclients 0  # 设置同一时间最大客户端连接数,默认无限制。当客户端连接到达上限, Redis会关闭新的连接
# 注意:日志级别开发期设置为verbose即可,生产环境中配置为notice,简化日志输出量,降低写日志IO的频度  
timeout 300 #  客户端闲置等待最大时长,达到最大值后关闭连接。如需关闭该功能,设置为 0  

多服务器快捷配置:

 导入并加载指定配置文件信息,用于快速创建redis公共配置较多的redis实例配置文件,便于维护

include /path/server-端口号.conf

相当于继承

8.1 主从复制

Redis 中,用户可以通过执行 SLAVEOF 命令或者设置 slaveof 选项,让一个服务器去复制另一个服务器,成为从服务器,以下三种方式是 完全等效 的:

  • 配置文件:在从服务器的配置文件中加入:slaveof <masterip> <masterport>
  • 启动命令:redis-server 启动命令后加入 --slaveof <masterip> <masterport>
  • 客户端命令:Redis 服务器启动后,直接通过客户端执行命令:slaveof <masterip> <masterport>,让该 Redis 实例成为从节点。

需要注意的是:主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。

在master端配置:

# -----master授权slave访问-------
 master客户端发送命令设置密码`requirepass <password>`
 master配置文件设置密码
`config set requirepass <password>`
`config get requirepass`

 slave客户端发送命令设置密码 `auth <password>`
 slave配置文件设置密码 `masterauth <password>`
 slave启动服务器设置密  `redis-server –a <password>`

# ----------主从断开连接-----------
 客户端发送命令:`slaveof no one `
## 说明:slave断开连接后,不会删除已有数据,只是不再接受master发送的数据  

8.2 哨兵与选举

redis相关阅读

  1. Redis(1)——5种基本数据结构 - https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/
  2. Redis(2)——跳跃表 - https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/
  3. Redis(3)——分布式锁深入探究 - https://www.wmyskxz.com/2020/03/01/redis-3/
  4. Reids(4)——神奇的HyperLoglog解决统计问题 - https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/
  5. Redis(5)——亿级数据过滤和布隆过滤器 - https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/
  6. Redis(6)——GeoHash查找附近的人https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/
  7. Redis(7)——持久化【一文了解】 - https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/
  8. Redis(8)——发布/订阅与Stream - [https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/](

9.6 集群操作

  1. 以集群的方式进入客户端

redis-cli -c -p 端口号

  1. 通过cluster nodes 命令查看集群信息

  2. redis cluster 如何分配这六个节点

一个集群至少要有三个主节点。

选项 --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。

  1. 什么是slots

一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

​ 节点 A 负责处理 0 号至 5500 号插槽。

​ 节点 B 负责处理 5501 号至 11000 号插槽。

​ 节点 C 负责处理 11001 号至 16383 号插槽

  1. 在集群中录入值

在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口.

redis-cli客户端提供了 –c 参数实现自动重定向。

如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。

不在一个slot下的键值,是不能使用mget,mset等多键操作。

可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去

  1. 查询集群中的值
  • CLUSTER KEYSLOT <key> 计算键 key 应该被放置在哪个槽上。
  • CLUSTER COUNTKEYSINSLOT <slot> 返回槽 slot 目前包含的键值对数量
  • CLUSTER GETKEYSINSLOT <slot> <count> 返回 count 个 slot 槽中的键
  1. 故障恢复

l 如果主节点下线?从节点能否自动升为主节点?

l 主节点恢复后,主从关系会如何?

l 如果所有某一段插槽的主从节点都当掉,redis服务是否还能继续?

redis.conf中的参数 cluster-require-full-coverage

redis客户端

Jedis api 在线网址:http://tool.oschina.net/uploads/apidocs/redis/clients/jedis/Jedis.html

redisson 官网地址:https://redisson.org/

redisson git项目地址:https://github.com/redisson/redisson

lettuce 官网地址:https://lettuce.io/

lettuce git项目地址:https://github.com/lettuce-io/lettuce-core

首先,在spring boot2之后,对redis连接的支持,默认就采用了lettuce。这就一定程度说明了lettuce 和Jedis的优劣。

概念:

Jedis:是老牌的Redis的Java实现客户端,提供了比较全面的Redis命令的支持,

Redisson:实现了分布式和可扩展的Java数据结构。

Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:
  Jedis:比较全面的提供了Redis的操作特性

Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

可伸缩:

Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。

Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

lettuce能够支持redis4,需要java8及以上。
lettuce是基于netty实现的与redis进行同步和异步的通信。

lettuce和jedis比较:
jedis使直接连接redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个jedis实例增加物理连接 ;

lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,StatefulRedisConnection是线程安全的,所以一个连接实例可以满足多线程环境下的并发访问,当然这也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

总结:
优先使用Lettuce,如果需要分布式锁,分布式集合等分布式的高级特性,添加Redisson结合使用,因为Redisson本身对字符串的操作支持很差。

9.7 集群的Jedis开发

public class JedisClusterTest {
    public static void main(String[] args) {

        Set<HostAndPort> set =new HashSet<HostAndPort>();
        set.add(new HostAndPort("192.168.31.211",6379));
        JedisCluster jedisCluster=new JedisCluster(set);
        jedisCluster.set("k1", "v1");
        System.out.println(jedisCluster.get("k1"));
    }
}

RedisTemplate

直接传对象的话就报 未序列化的错误

Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对redis底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。
官网:http://projects.spring.io/spring-data-redis/
项目地址:https://github.com/spring-projects/spring-data-redis

一、spring-data-redis功能介绍

jedis客户端在编程实施方面存在如下不足:

1)connection管理缺乏自动化,connection-pool的设计缺少必要的容器支持。
2)数据操作需要关注“序列化”/“反序列化”,因为jedis的客户端API接受的数据类型为string和byte,对结构化数据(json,xml,pojo等)操作需要额外的支持。https://www.cnblogs.com/duanxz/p/3511695.html
3)事务操作纯粹为硬编码。
4)pub/sub功能,缺乏必要的设计模式支持,对于开发者而言需要关注的太多。

spring-data-redis针对jedis提供了如下功能:

  • 1.连接池自动管理,提供了一个高度封装的“RedisTemplate”类
  • 2.针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
    • ValueOperations:简单K-V操作
    • SetOperations:set类型数据操作
    • ZSetOperations:zset类型数据操作
    • HashOperations:针对map类型的数据操作
    • ListOperations:针对list类型的数据操作
  • 3.提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations:
    • BoundValueOperations
    • BoundSetOperations
    • BoundListOperations
    • BoundSetOperations
    • BoundHashOperations
  • 4.将事务操作封装,有容器控制。
  • 5.针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)
    • JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。
    • StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。
    • JacksonJsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】
    • OxmSerializer:提供了将javabean与xml之间的转换能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存储的数据将是xml工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。【需要spring-oxm模块的支持】
    • 针对“序列化和发序列化”中JdkSerializationRedisSerializer和StringRedisSerializer是最基础的策略,原则上,我们可以将数据存储为任何格式以便应用程序存取和解析(其中应用包括app,hadoop等其他工具),不过在设计时仍然不推荐直接使用“JacksonJsonRedisSerializer”和“OxmSerializer”,因为无论是json还是xml,他们本身仍然是String。如果你的数据需要被第三方工具解析,那么数据应该使用StringRedisSerializer而不是JdkSerializationRedisSerializer。如果你的数据格式必须为json或者xml,那么在编程级别,在redisTemplate配置中仍然使用StringRedisSerializer,在存储之前或者读取之后,使用“SerializationUtils”工具转换转换成json或者xml
  • 6.基于设计模式,和JMS开发思路,将pub/sub的API设计进行了封装,使开发更加便捷。
  • 7.spring-data-redis中,并没有对sharding提供良好的封装,如果你的架构是基于sharding,那么你需要自己去实现,这也是sdr和jedis相比,唯一缺少的特性。
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnection(factory);//LettuceConnectionFactory
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.set

redis-template用法

序列化:https://www.cnblogs.com/duanxz/p/3511695.html

  • transient修饰的属性,不会被序列化
  • 静态static的属性,不会序列化
  • 实现Serializable接口的时候,一定要给这个serialVersionID赋值。序列化的时候没有某个属性,反序列话的时候增加了某个属性,重新计算的UID就不一样了。https://blog.csdn.net/u014750606/article/details/80040130
  • 当属性是对象的时候,镀锡也有实现序列化的接口
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
String
//set void set(K key, V value);
redisTemplate.opsForValue().set("num","123");
redisTemplate.opsForValue().get("num")  输出结果为123

//set void set(K key, V value, long timeout, TimeUnit unit); 
redisTemplate.opsForValue().set("num","123",10, TimeUnit.SECONDS);
redisTemplate.opsForValue().get("num")设置的是10秒失效,十秒之内查询有结果,十秒之后返回为null
		TimeUnit.DAYS          //天
		TimeUnit.HOURS         //小时
		TimeUnit.MINUTES       //分钟
		TimeUnit.SECONDS       //秒
		TimeUnit.MILLISECONDS  //毫秒 

//set void set(K key, V value, long offset);
//覆写(overwrite)给定 key 所储存的字符串值,从偏移量 offset 开始
template.opsForValue().set("key","hello world");
template.opsForValue().set("key","redis", 6);
System.out.println("***************"+template.opsForValue().get("key"));
结果:***************hello redis

//get V get(Object key);
template.opsForValue().set("key","hello world");
System.out.println("***************"+template.opsForValue().get("key"));
结果:***************hello world

//getAndSet V getAndSet(K key, V value); 
//设置键的字符串值并返回其旧值
template.opsForValue().set("getSetTest","test");
System.out.println(template.opsForValue().getAndSet("getSetTest","test2"));
结果:test

//append Integer append(K key, String value);
//如果key已经存在并且是一个字符串,则该命令将该值追加到字符串的末尾。如果键不存在,则它被创建并设置为空字符串,因此APPEND在这种特殊情况下将类似于SET。
template.opsForValue().append("test","Hello");
System.out.println(template.opsForValue().get("test"));
template.opsForValue().append("test","world");
System.out.println(template.opsForValue().get("test"));
Hello
Helloworld

//size Long size(K key);返回key所对应的value值得长度
template.opsForValue().set("key","hello world");
System.out.println("***************"+template.opsForValue().size("key"));
***************11
List
//Long size(K key);返回存储在键中的列表的长度。如果键不存在,则将其解释为空列表,并返回0。当key存储的值不是列表时返回错误。

System.out.println(template.opsForList().size("list"));


//Long leftPush(K key, V value);
将所有指定的值插入存储在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建为空列表。(从左边插入)

template.opsForList().leftPush("list","java");
template.opsForList().leftPush("list","python");
template.opsForList().leftPush("list","c++");
返回的结果为推送操作后的列表的长度


//Long leftPushAll(K key, V... values);
批量把一个数组插入到列表中

String[] strs = new String[]{"1","2","3"};
template.opsForList().leftPushAll("list",strs);
System.out.println(template.opsForList().range("list",0,-1));
[3, 2, 1]

//Long rightPush(K key, V value);
将所有指定的值插入存储在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建为空列表。(从右边插入)

template.opsForList().rightPush("listRight","java");
template.opsForList().rightPush("listRight","python");
template.opsForList().rightPush("listRight","c++");

//Long rightPushAll(K key, V... values);

String[] strs = new String[]{"1","2","3"};
template.opsForList().rightPushAll("list",strs);
System.out.println(template.opsForList().range("list",0,-1));
[1, 2, 3]

//void set(K key, long index, V value);
在列表中index的位置设置value值

System.out.println(template.opsForList().range("listRight",0,-1));
template.opsForList().set("listRight",1,"setValue");
System.out.println(template.opsForList().range("listRight",0,-1));
[java, python, oc, c++]
[java, setValue, oc, c++]

//Long remove(K key, long count, Object value);
从存储在键中的列表中删除等于值的元素的第一个计数事件。
计数参数以下列方式影响操作:
count> 0:删除等于从头到尾移动的值的元素。
count <0:删除等于从尾到头移动的值的元素。
count = 0:删除等于value的所有元素。

System.out.println(template.opsForList().range("listRight",0,-1));
template.opsForList().remove("listRight",1,"setValue");//将删除列表中存储的列表中第一次次出现的“setValue”。
System.out.println(template.opsForList().range("listRight",0,-1));
[java, setValue, oc, c++]
[java, oc, c++]

//V index(K key, long index);
根据下表获取列表中的值,下标是从0开始的

System.out.println(template.opsForList().range("listRight",0,-1));
System.out.println(template.opsForList().index("listRight",2));
[java, oc, c++]
c++

//V leftPop(K key);
弹出最左边的元素,弹出之后该值在列表中将不复存在

System.out.println(template.opsForList().range("list",0,-1));
System.out.println(template.opsForList().leftPop("list"));
System.out.println(template.opsForList().range("list",0,-1));
[c++, python, oc, java, c#, c#]
c++
[python, oc, java, c#, c#]

//V rightPop(K key);
弹出最右边的元素,弹出之后该值在列表中将不复存在

System.out.println(template.opsForList().range("list",0,-1));
System.out.println(template.opsForList().rightPop("list"));
System.out.println(template.opsForList().range("list",0,-1));
[python, oc, java, c#, c#]
c#
[python, oc, java, c#]



Hash
//Long delete(H key, Object... hashKeys);
删除给定的哈希hashKeys

System.out.println(template.opsForHash().delete("redisHash","name"));
System.out.println(template.opsForHash().entries("redisHash"));
1
{class=6, age=28.1}

//Boolean hasKey(H key, Object hashKey);
确定哈希hashKey是否存在

System.out.println(template.opsForHash().hasKey("redisHash","666"));
System.out.println(template.opsForHash().hasKey("redisHash","777"));
true
false

//HV get(H key, Object hashKey);
从键中的哈希获取给定hashKey的值

System.out.println(template.opsForHash().get("redisHash","age"));
26

//Set<HK> keys(H key);
获取key所对应的散列表的key

System.out.println(template.opsForHash().keys("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
[name, class, age]

//Long size(H key);
获取key所对应的散列表的大小个数

System.out.println(template.opsForHash().size("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
3

//void putAll(H key, Map<? extends HK, ? extends HV> m);
使用m中提供的多个散列字段设置到key对应的散列表中

Map<String,Object> testMap = new HashMap();
testMap.put("name","666");
testMap.put("age",27);
testMap.put("class","1");
template.opsForHash().putAll("redisHash1",testMap);
System.out.println(template.opsForHash().entries("redisHash1"));
{class=1, name=jack, age=27}

//void put(H key, HK hashKey, HV value);
设置散列hashKey的值

template.opsForHash().put("redisHash","name","666");
template.opsForHash().put("redisHash","age",26);
template.opsForHash().put("redisHash","class","6");
System.out.println(template.opsForHash().entries("redisHash"));
{age=26, class=6, name=666}

//List<HV> values(H key);
获取整个哈希存储的值根据密钥

System.out.println(template.opsForHash().values("redisHash"));
[tom, 26, 6]

//Map<HK, HV> entries(H key);
获取整个哈希存储根据密钥

System.out.println(template.opsForHash().entries("redisHash"));
{age=26, class=6, name=tom}

//Cursor<Map.Entry<HK, HV>> scan(H key, ScanOptions options);
使用Cursor在key的hash中迭代,相当于迭代器。

Cursor<Map.Entry<Object, Object>> curosr = template.opsForHash().scan("redisHash", 
  ScanOptions.ScanOptions.NONE);
    while(curosr.hasNext()){
        Map.Entry<Object, Object> entry = curosr.next();
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
age:27
class:6
name:666
Set
//Long add(K key, V... values);
无序集合中添加元素,返回添加个数
也可以直接在add里面添加多个值 如:template.opsForSet().add("setTest","aaa","bbb")

String[] strs= new String[]{"str1","str2"};
System.out.println(template.opsForSet().add("setTest", strs));
2

// Long remove(K key, Object... values);
移除集合中一个或多个成员

String[] strs = new String[]{"str1","str2"};
System.out.println(template.opsForSet().remove("setTest",strs));
2

// V pop(K key);
移除并返回集合中的一个随机元素

System.out.println(template.opsForSet().pop("setTest"));
System.out.println(template.opsForSet().members("setTest"));
bbb
[aaa, ccc]

// Boolean move(K key, V value, K destKey);
将 member 元素从 source 集合移动到 destination 集合

template.opsForSet().move("setTest","aaa","setTest2");
System.out.println(template.opsForSet().members("setTest"));
System.out.println(template.opsForSet().members("setTest2"));
[ccc]
[aaa]

// Long size(K key);
无序集合的大小长度

System.out.println(template.opsForSet().size("setTest"));
1

//Set<V> members(K key);
返回集合中的所有成员

System.out.println(template.opsForSet().members("setTest"));
[ddd, bbb, aaa, ccc]

// Cursor<V> scan(K key, ScanOptions options);
遍历set

Cursor<Object> curosr = template.opsForSet().scan("setTest", ScanOptions.NONE);
  while(curosr.hasNext()){
     System.out.println(curosr.next());
  }
ddd
bbb
aaa
ccc
ZSet
//Boolean add(K key, V value, double score);
新增一个有序集合,存在的话为false,不存在的话为true

System.out.println(template.opsForZSet().add("zset1","zset-1",1.0));
true

// Long add(K key, Set<TypedTuple<V>> tuples);
新增一个有序集合

ZSetOperations.TypedTuple<Object> objectTypedTuple1 = new DefaultTypedTuple<>("zset-5",9.6);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>("zset-6",9.9);
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<ZSetOperations.TypedTuple<Object>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
System.out.println(template.opsForZSet().add("zset1",tuples));
System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]

//Long remove(K key, Object... values);
从有序集合中移除一个或者多个元素

System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().remove("zset1","zset-6"));
System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]
1
[zset-1, zset-2, zset-3, zset-4, zset-5]

// Long rank(K key, Object o);
返回有序集中指定成员的排名,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().rank("zset1","zset-2"));
[zset-2, zset-1, zset-3, zset-4, zset-5]
0   //表明排名第一

//Set<V> range(K key, long start, long end);
通过索引区间返回有序集合成指定区间内的成员,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-2, zset-1, zset-3, zset-4, zset-5]

//Long count(K key, double min, double max);
通过分数返回有序集合指定区间内的成员个数

System.out.println(template.opsForZSet().rangeByScore("zset1",0,5));
System.out.println(template.opsForZSet().count("zset1",0,5));
[zset-2, zset-1, zset-3]
3

//Long size(K key);
获取有序集合的成员数,内部调用的就是zCard方法

System.out.println(template.opsForZSet().size("zset1"));
6

// Double score(K key, Object o);
获取指定成员的score值

System.out.println(template.opsForZSet().score("zset1","zset-1"));
2.2

//Long removeRange(K key, long start, long end);
移除指定索引位置的成员,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset2",0,-1));
System.out.println(template.opsForZSet().removeRange("zset2",1,2));
System.out.println(template.opsForZSet().range("zset2",0,-1));
[zset-1, zset-2, zset-3, zset-4]
2
[zset-1, zset-4]

//Cursor<TypedTuple<V>> scan(K key, ScanOptions options);
遍历zset

Cursor<ZSetOperations.TypedTuple<Object>> cursor = template.opsForZSet().scan("zzset1", ScanOptions.NONE);
    while (cursor.hasNext()){
       ZSetOperations.TypedTuple<Object> item = cursor.next();
       System.out.println(item.getValue() + ":" + item.getScore());
    }
zset-1:1.0
zset-2:2.0
zset-3:3.0
zset-4:6.0

所有的对象需要序列化。在企业中,所有的pojo都会序列化。如果不想用java的序列化,就要编写自己的redisTemplate

真实的开发一般都使用json来传递对象

User user = new User("123",12);
redisTemplate.opsForValue().set("user",user);
sout(redisTemplate.opsForValue().get("user"))

企业案例

高并发秒杀超卖
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock>0){
    int realStock = stock-1;
    strngRedisTemplate.opsForValue().set("stock",realStock+"");
    //成功
}else{
    //失败
}

解决方案:

setnx若存在不操作if not exists

思路:只是练习,不能实用

String lockKey = "lockKey";
try{
    //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"hh")
      //  stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
    //上两句应该合并才不会出错
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"hh",10,TimeUnit.SECONDS);//重载
    
    
        if(!result) return "error";

    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if(stock>0){
        int realStock = stock-1;
        strngRedisTemplate.opsForValue().set("stock",realStock+"");
        //成功
    }else{
        //失败
    }
}finally{
    stringRedisTemplate.delete(lockKey);
}


return "end";

分布式锁:

https://blog.csdn.net/wuzhiwei549/article/details/80692278

setnx不支持设置时间,所以需要后面跟上expire,但这样就非原子了,容易执行一般就挂了,死锁了。而set方法支持设置超时时间set(lock_sale_商品ID,1,30,NX)。还有个问题是如何给锁续命的问题,得用守护线程,当被守护的线程挂了,守护线程也就失去作用了。

其他解决方法:zk的有序临时结点

Redission


https://www.bilibili.com/video/BV16K411J754?p=3

热点数据

例如使用 Zset 数据结构,存储 Key 的访问次数/最后访问时间作为 Score,最后做排序,来淘汰那些最少访问的 Key。

如果企业级应用,可以参考:[阿里云的 Redis 混合存储版][1]

会话维持 Session

会话维持 Session 场景,即使用 Redis 作为分布式场景下的登录中心存储应用。每次不同的服务在登录的时候,都会去统一的 Redis 去验证 Session 是否正确。但是在微服务场景,一般会考虑 Redis + JWT 做 Oauth2 模块。

其中 Redis 存储 JWT 的相关信息主要是留出口子,方便以后做统一的防刷接口,或者做登录设备限制等。

表缓存

Redis 缓存表的场景有黑名单、禁言表等。访问频率较高,即读高。根据业务需求,可以使用后台定时任务定时刷新 Redis 的缓存表数据。

消息队列 list

主要使用了 List 数据结构。
List 支持在头部和尾部操作,因此可以实现简单的消息队列

  1. 发消息:在 List 尾部塞入数据。
  2. 消费消息:在 List 头部拿出数据。

同时可以使用多个 List,来实现多个队列,根据不同的业务消息,塞入不同的 List,来增加吞吐量。

可视化页面:Anoter Redis DeskTop Manager

常见面试题

为啥Redis那么快?

img

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用多路I/O复用模型,非阻塞NIO;
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

我可以问一下啥是上下文切换么?

线程切换时线程当前的状态,包括栈等

那他是单线程的,我们现在服务器都是多核的,那不是很浪费?
是的他是单线程的,但是,我们可以通过在单机开多个Redis实例嘛。
既然提到了单机会有瓶颈,那你们是怎么解决这个瓶颈的?
我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node
这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
哦?那问题就来了,他们之间是怎么进行数据交互的?以及Redis是怎么进行持久化的?Redis数据都在内存中,一断电或者重启不就木有了嘛?
是的,持久化的话是Redis高可用中比较重要的一个环节,因为Redis数据在内存的特性,持久化必须得有,我了解到的持久化是有两种方式的。

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备AOF更适合做热备,比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧,这灾备也就是异地容灾,地球毁灭他没办法。
tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。
那这两种机制各自优缺点是啥?
我先说RDB
优点:
他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。

还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。
我们再来说说AOF
优点:
上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。
AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

缺点:
一样的数据,AOF文件比RDB还要大。
AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。
那两者怎么选择?

小孩子才做选择,我全都要,你单独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么时候第一时间用RDB恢复,然后AOF做数据补全,真香!冷备热备一起上,才是互联网时代一个高健壮性系统的王道。
看不出来年纪轻轻有点东西的呀,对了我听你提到了高可用,Redis还有其他保证集群高可用的方式么?
!!!晕 自己给自己埋个坑(其实是明早就准备好了,故意抛出这个词等他问,就怕他不问)。
假装思考一会(不要太久,免得以为你真的不会),哦我想起来了,还有哨兵集群sentinel
哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用
为啥必须要三个实例呢?我们先看看两个哨兵会咋样。

imgimg

master宕机了 s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障,但是这个时候也需要大多数哨兵都是运行的。
那这样有啥问题呢?M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2个裸屌了,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移就是不执行。
经典的哨兵集群是这样的:

imgimg

M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。
暖男我,小的总结下哨兵组件的主要功能:

    • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
    • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
    • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
    • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

我记得你还提到了主从同步,能说一下主从之间的数据怎么同步的么?
面试官您的记性可真是一级棒呢,我都要忘了你还记得,我特么谢谢你,提到这个,就跟我前面提到的数据持久化的RDBAOF有着比密切的关系了。
我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

img

回归正题,他们数据怎么同步的呢?
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。
数据传输的时候断网了或者服务器挂了怎么办啊?
传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的。
大家需要记得的就是,RDB快照的数据生成的时候,缓存区也必须同时开始接受新请求,不然你旧的数据过去了,你在同步期间的增量数据咋办?是吧?
那说了这么多你能说一下他的内存淘汰机制么,来手写一下LRU代码?

手写LRU?你是不是想直接跳起来说一句:Are U F*k Kidding me?
这个问题是我在蚂蚁金服三面的时候亲身被问过的问题,不知道大家有没有被怼到过这个问题。
Redis的过期策略,是有
定期删除+惰性删除*两种。
定期好理解,默认100s就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
为啥不扫描全部设置了过期时间的key呢?
假如Redis里面所有的key都有过期时间,都扫描一遍?那太恐怖了,而且我们线上基本上也都是会设置一定的过期时间的。全扫描跟你去查数据库不带where条件不走索引全表扫描一样,100s一次,Redis累都累死了。
如果一直没随机到很多key,里面不就存在大量的无效key了?
好问题,惰性删除,见名知意,惰性嘛,我不主动删,我懒,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样。
最后就是如果的如果,定期没删,我也没查询,那可咋整?
内存淘汰机制
官网上给到的内存淘汰机制是以下几个:

    • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
    • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
    • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
    • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
    • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
      如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

至于LRU我也简单提一下,手写实在是太长了,大家可以去Redis官网看看,我把近视LUR效果给大家看看
tip:Redis为什么不使用真实的LRU实现是因为这需要太多的内存。不过近似的LRU算法对于应用而言应该是等价的。使用真实的LRU算法与近似的算法可以通过下面的图像对比。

img

你可以看到三种点在图片中, 形成了三种带.

    • 浅灰色带是已经被回收的对象。
    • 灰色带是没有被回收的对象。
    • 绿色带是被添加的对象。
    • LRU实现的理论中,我们希望的是,在旧键中的第一半将会过期。RedisLRU算法则是概率的过期旧的键。

你可以看到,在都是五个采样的时候Redis 3.0比Redis 2.8要好,Redis2.8中在最后一次访问之间的大多数的对象依然保留着。使用10个采样大小的Redis 3.0的近似值已经非常接近理论的性能。
注意LRU只是个预测键将如何被访问的模型。另外,如果你的数据访问模式非常接近幂定律,大部分的访问将集中在一个键的集合中,LRU的近似算法将处理得很好。
其实在大家熟悉的LinkedHashMap中也实现了Lru算法的,实现如下:

imgimg

当容量超过100时,开始执行LRU策略:将最近最少未使用的 TimeoutInfoHolder 对象 evict 掉。
真实面试中会让你写LUR算法,你可别搞原始的那个,那真TM多,写不完的,你要么怼上面这个,要么怼下面这个,找一个数据结构实现下Java版本的LRU还是比较容易的,知道啥原理就好了。

imgimg

©️2022 CSDN 皮肤主题:撸撸猫 设计师:马嘣嘣 返回首页

打赏作者

hancoder

给个打赏吧

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值