RBAC简介

news2024/11/15 21:09:08

RBAC

BAC基于角色的访问控制,RBAC认为权限授权的过程可以抽象地概括为:Who是否可以对What进行How的访问操作

RBAC简介

基于角色的权限访问控制模型

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限。RBAC通过定义角色的权限,并
对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型),极大
地方便了权限的管理:

  • User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
  • Role(角色):不同角色具有不同的权限
  • Permission(权限):访问权限
  • 用户-角色映射:用户和角色之间的映射关系
  • 角色-权限映射:角色和权限之间的映射

RBAC支持三个著名的安全原则:最小权限原则、责任分离原则和数据抽象原则

  • 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小权限集合
  • 责任分离原则:可以通过调用相互独立互斥的角色来共同完成敏感的任务,例如要求一个计账员和财务管理员共同参与统一过账操作
  • 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写、执行权限
    在这里插入图片描述
    优点:
  • 简化了用户和权限的关系
  • 易扩展、易维护
    缺点:
  • RBAC模型没有提供操作顺序的控制机制,这一缺陷使得RBAC模型很难适应哪些对操作次序有严格要求的系统

RBAC的3种模型

  • RBAC0,是最简单、最原始的实现方式,也是其RBAC模型的基础
  • RBAC1基于RBAC0模型,引入了角色间的继承关系,即角色上有了上下级的区别。
  • RBAC2基于RBAC0模型的基础上,进行了角色的访问控制。

简介JWT

http协议无状态的,所以需要sessionId或token的鉴权机制,jwt的token认证机制不需要在服务端再保留用户的认证信息或会话信息。这就意味着基于jwt认证机制的应用程序不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,jwt更适用于分布式应用

1、前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
2、后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
3、后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
4、前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
5、后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等
6、验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

JWT的优势
1、简洁:JWT Token数据量小,传输速度也很快
2、因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支

3、不需要在服务端保存会话信息,也就是说不依赖于cookie和session,所以没有了传统session认证的弊端,特别适用于分布式微服务
4、单点登录友好:使用Session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题
5、适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要Cookie 保存 SessionId),所以不适合移动端

jwt的组成部分
标准的jwt令牌分为三部分,分别是Header、payload、signature;在token字符串中使用.进行分割

  • Header的组成部分包括两点:参数类型jwt,签名的算法hs256
  • Payload的组成就是登陆用户的一些信息,和token发行和失效时间等;这些内容里面有一些是标准字段,你也可以添加其它需要的内容
  • Signature是先用Base64编码的header和payload,再用加密算法加密一下,加密的时候要放进去一Secret,这个相当于是一个密码,这个密码秘密地存储在服务端

JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据。
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值
    JWT工作原理
    在这里插入图片描述

SpringBoot项目中登录流程

1、在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算
法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过
期时间就是登录失效的时间
2、将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端
3、前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串
4、后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录

SpringBoot原理

SpringBoot启动原理

创建springbootApplication对象

  1. 创建springbootApplication对象springboot容器初始化操作
  2. 获取当前应用的启动类型。
    1:通过判断当前classpath是否加载servlet类,返回servlet web启动方式
    2:webApplicationType三种类型:reactive响应式启动、none即不嵌入web容器启动,
    springboot放在外部服务器运行、servlet基于web容器进行启动
  3. 读取springboot下的META-INF/spring.factories文件,获取对应的ApplicationContextInitializer装
    配到集合
  4. 读取springboot下的META-INF/spring.factories文件,获取对应的ApplicationListener装配到集合
  5. mainApplicationClass获取当前运行的主函数
  6. 调用springbootApplication对象的run方法,实现启动,返回当前容器的上下文
  7. 调用run方法启动
  8. StopWatch stopWatch = new StopWatch()记录项目启动时间
  9. getRunListeners读取META-INF/spring.factores,将SpringApplicationRunListeners类型存到集合中
  10. listeners.starting()循环调用starting方法
  11. prepareEnvironment(listeners, applicationArguments);将配置文件读取到容器中。读取多数据源classpath:/、classpath:/config/、file:./、file:./config/底下。其中classpath是读取编译后的,file是读取编译前的支持yml、yaml、xml和properties
  12. Banner printedBanner = printBanner(environment);开始打印banner图,就是spring boot启动最开头的logo图案
  13. AnnotationConfigServletWebServerApplicationContext初始化对象
  14. 刷新上下文调用注解,refreshContext(context);
  15. 创建tomcat
  16. 加载springmvc
  17. 刷新后的方法,空方法,给用户自定义重写afterRefresh方法
  18. stopWatch.stop();结束计时
  19. 使用广播和回调机制告诉监听者springboot容器已经启动化成功,listeners.started(context);
  20. 使用广播和回调机制告诉监听者springboot容器已经启动化成功,listeners.started(context);
  21. 返回上下文

@SpringBootApplication组合注解

@SpringBootApplication除去元注解真正起作用的注解有@SpringBootConfiguration、
@EnableAutoConfiguration和@ComponentScan
注解是输出logo后,通过ApplicationContext初始化后的refreshContext方法注入实现的
@SpringBootConfiguration作用就是将当前类申明为配置类,同时还可以使用@bean注解将类以方法
的形式实例化到spring容器,而方法名就是实例名
@ComponentScan
@ComponentScan作用就是扫描当前包以及子包,将有@Component,@Controller,@Service,
@Repository等注解的类注册到容器中,以便调用
如果@ComponentScan不指定basePackages,那么默认扫描当前包以及其子包,而@SpringBootApplication里的@ComponentScan就是默认扫描,所以我们一般都是把springboot启动类放在最外层,以便扫描所有的类
@EnableAutoConfiguration
@EnableAutoConfiguration工作原理主要就是通过内部的方法,扫描classpath的METAINF/spring.factories配置文件,将其中的org.springframework.boot.autoconfigure.
EnableAutoConfiguration 对应的配置项实例化并且注册到spring容器
对应classpath的META-INF/spring/目录下配置文件
场景启动器starter
使用Spring框架开发项目带来的一些的问题

  • 依赖导入问题:每个项目都需要来单独维护自己所依赖的jar包,在项目中使用到什么功能就需要引入什么样的依赖。手动导入依赖容易出错,且无法统一集中管理
  • 配置繁琐:在引入依赖之后需要做繁杂的配置,并且这些配置是每个项目来说都是必要的,这些配置重复且繁杂,在不同的项目中需要进行多次重复开发,这在很大程度上降低了开发效率
    在导入的starter之后,SpringBoot主要完成了两件事情:
  • 相关组件的自动导入
  • 相关组件的自动配置
    这两件事情统一称为SpringBoot的自动配置
    starter的命名规范
  • 官方命名空间采用前缀定义方式spring-boot-starter-,例如spring-boot-starter-web、springboot-starter-jdbc
  • 自定义命名空间采用后缀定义方式-spring-boot-starter,例如mybatis-spring-boot-starter

starter的整体实现逻辑主要由两个基本部分组成:

  • xxxAutoConfiguration自动配置类,对某个场景下需要使用到的一些组件进行自动注入,并利用xxxProperties类来进行组件相关配置
  • xxxProperties某个场景下所有可配置属性的集成,在配置文件中配置可以进行属性值的覆盖

按照SpringBoot官方的定义,Starer的作用就是依赖聚合,因此直接在starter内部去进行代码实现是不
符合规定的,starter应该只起到依赖导入的作用,而具体的代码实现应该去交给其他模块来实现,然后
在starter中去引用该模块即可

自动配置类的工作过程

首先容器会根据当前不同的条件判断,决定这个配置类是否生效

  • 一但这个配置类生效;这个配置类就会给容器中添加相应组件
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着,配置文件可以配置什么内容,可以参照该前缀对应的属性类中的属性字段
    SpringBoot自动配置
    SpringBoot启动会加载大量的自动配置类
  • 首先可以看需要的功能有没有在SpringBoot默认写好的自动配置类当中
  • 再来看这个自动配置类中到底配置了哪些组件;只要要用的组件存在在其中,就不需要再手动配置了
    给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。只需要在配置文件中指
    定这些属性的值即可
    • xxxxAutoConfigurartion自动配置类;给容器中添加组件
    • xxxxProperties:封装配置文件中相关属性
      @Conditional派生注解
      @Conditional派生注解作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置里的所
      有内容才生效
      @ConditionalOnMissingBean容器中不存在指定的Bean
      @ConditionalOnBean容器中是否存在指定的Bean
      @ConditionalOnMissingClass系统中没有指定的类
      @ConditionalOnClass系统中有指定的类
      @ConditionalOnProperty系统中指定的属性是否有指定的值
      … …

数据库设计

数据库设计是指对于一个给定的应用环境,构造最优的数据库模式,建立数据库及其应用系统,使之能够有效地存储数据,满足各种用户的应用需求,重要的是信息要求和处理要求。
数据库设计方法

  • 试凑法,依靠设计人员的工作经验
  • 规划化设计法,3NF
  • 计算机辅助设计法。PowerDesigner
    数据库设计阶段
  1. 需求分析。信息要求和处理要求,自顶向下的结构化分析法
  2. 逻辑结构设计。概念模型ER图
  3. 概念结构设计。数据模型:关系和非关系型模型
  4. 物理设计。存储安排和方法选择
  5. 实施阶段。编写模式、装入数据
    数据库SQL优化
    1、创建索引。对于查询占主要的应用来说,索引显得尤为重要。如果不加索引的话,那么查找任何哪怕只是一条特定的数据都会进行一次全表扫描,如果一张表的数据量很大而符合条件的结果又很少,那么不加索引会引起致命的性能下降。但是也不是什么情况都非得建索引不可,比如性别可能就只有两个值,建索引不仅没什么优势,还会影响到更新速度,这被称为过度索引。
    2、复合索引。由于mysql查询每次只能使用一个索引,所以虽然这样已经相对不做索引时全表扫描提高了很多效率,但是如果在多个列上创建复合索引的话将带来更高的效率。如果创建了area、age和salary的复合索引,那么其实相当于创建了(area,age,salary)、(area,age)、(area)三个索引,这被称为最佳左前缀特性。因此在创建复合索引时应该将最常用作限制条件的列放在最左边,依次递减。
    3、索引不会包含有NULL值的列。只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以在数据库设计时不要让字段的默认值为NULL。
    4、使用短索引。对字符串列进行索引,如果可能应该指定一个前缀长度。例如有一个CHAR(255)的列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
    5、排序的索引问题。mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
    6、like语句操作。一般情况下不鼓励使用like操作,如果非使用不可,like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。
    7、不要在列上进行运算。例如select * from users where YEAR(adddate)<2023。将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此可以改成select * from users where adddate<‘2023-01-01’;
    8、不使用NOT IN和<>操作。NOT IN和<>操作都不会使用索引将进行全表扫描。NOT IN可以NOT EXISTS代替,id<>3则可使用id>3 or id<3来代替。
    SQL语句优化
    1、对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
    2、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如select id from t where num is null,最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库。不要以为 NULL 不需要空间。
    3、应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。
    4、应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描,如将or查询修改为集合合并操作union
    5、in 和 not in 也要慎用,否则会导致全表扫描
    6、模糊查询可能会将导致全表扫描。例如like ‘%abc%’,若要提高效率,可以考虑全文检索。
    7、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。
    8、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
    9、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
    10、对于多张大数据量的表JOIN,要先分页再JOIN,否则逻辑读会很高,性能很差。
    11、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
    12、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连 接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
    13、任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
    14、尽量避免大事务操作,提高系统并发能力
    15、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

RBAC数据表设计

数据库 create database test default character set utf8mb4;
表和表之间的关系:1对1、1对多或者多对1、多对多
1对1实现方式
具体的实现方式有2种:共享主键和唯一外键。例如员工信息和登录账号
共享主键

create table if not exists tb_emps(
id bigint primary key auto_increment comment '雇员编号',
name varchar(32) not null comment '姓名',
sex boolean default 1 comment '性别',
education varchar(32),
birth date comment '出生日期',
hire_date date comment '入职日期',
salary numeric(8,2) comment '月薪'
)engine=innodb default charset utf8 comment '雇员信息';
create table if not exists tb_users(
id bigint primary key comment '用户编号',
foreign key(id) references tb_emps(id) on delete cascade,
username varchar(32) not null unique comment '用户名称',
password varchar(32) not null comment '用户口令',
create_time datetime default now() comment '创建时间',
update_time timestamp default current_timestamp on update
current_timestamp comment '修改时间',
salt varchar(32) comment 'md5加密的盐值',
user_locked boolean default 0 comment '账户是否锁定'
)engine=innodb default charset utf8 comment '登录账户';

唯一外键

create table if not exists tb_emp(
id bigint primary key auto_increment,
......
)engine=innodb default charset utf8;
create table if not exists tb_user(
id bigint primary key auto_increment,
... ...
emp_id bigint not null unique,
foreign key(emp_id) references tb_emp(id) on delete cascade
)engine=innodb default charset utf8;

1对多或者多对1
实现方式就是主外键关联,在关系型数据库使用最多的,例如雇员和部门

create table tb_dept(
id bigint primary key auto_increment,
name varchar(32) not null,
address varchar(50)
);
create table tb_emp(
id bigint primary key auto_increment,
name varchar(32) not null,
... ...,
dept_id bigint,
foreign key(dept_id) references tb_dept(id)
);

多对多
实际上关系型数据库中不直接支持多对多关系,必须引入中间表。例如学生选课

create table tb_student(
id bigint primary key auto_increment,
... ....
);
create table tb_course(
id bigint primary key auto_increment,
......
);
create table tb_choice(
student_id bigint not null,
course_id bigint not null,
primary key(student_id,course_id),
foreign key(student_id) references tb_student(id) on delete cascade,
foreign key(course_id) references tb_course(id) on delete cascade
);

权限控制表的实现

  • 用户信息表用于存储登录用户相关数据,例如用户名、口令等。主要用于判断访问的用户是否为某个用户
create table tb_users(
id bigint primary key auto_increment comment '代理主键'
username varchar(32) not null,
password varchar(32) not null,
... ...
);
  • 角色表,角色可以理解为一组权限的别名,不等于职位。
create table tb_roles(
id bigint primary key auto_increment,
title varchar(32) not null unique,
... ...
);
  • 权限表,系统中为了简化开发,经常和菜单表合并,从而实现方便定义的目的。在具体的应用中通过菜单向是否显示以达到权限控制的目的。具体应用中针对菜单需要考虑使用无限级分类
create table tb_privs(
id bigint primary key auto_increment,
title varchar(32) not null comment '菜单项标题',
url varchar(50) comment '点击菜单后跳转的目标地址',
parent_id bigint comment '用于表示当前菜单项是哪个菜单项的子菜单',
foreign key(parent_id) references tb_privs(id) on delete cascade,
chilren boolean default 0 comment '不是必须的列,是为了编程方便引入的额外
列,用于表示是否有子菜单项'
-- 还可有其它和编程相关的列,例如层级等
);

样例数据
在这里插入图片描述

  • 用于表示用户和角色之间关系的表,因为用户和角色之间是多对多关联,所以必须引入中间表
create table tb_users_roles(
user_id bigint not null,
role_id bigint not null,
primary key(user_id,role_id),
foreign key(user_id) references tb_users(id) on delete cascade,
foreign key(role_id) references tb_roles(id) on delete cascade
);
  • 用于表示角色和权限之间关系的表,因为角色和权限之间是多对多关联,所以必须引入中间表
create table tb_roles_privs(
role_id bigint not null,
priv_id bigint not null,
primary key(role_id,priv_id),
foreign key(role_id) references tb_roles(id),
foreign key(priv_id) references tb_privs(id)
);
  • 根据业务需求,还有可能会出现需要将一些权限直接授予给某个用户的问题,如果需要支持这个功能,还需要创建用户和权限之间的中间表

整合开发

整合MyBatis开发的方式有3种:原生MyBatis、MybatisPlus和统一Mapper

Spring Data

SpringData 是Spring 的一个子项目。Spring 官方提供一套数据层综合解决方案,用于简化数据库访问,支持 NoSQL 和关心数据库存储。包括 SpringDataJPA、SpringDataRedis、SpringDataSolr、SpringDataElasticsearch、SpringDataMongodb 等框e架。
注册用户时,用户输入手机号码,则需要对手机号码进行验证,如何实现?

具体实现:通过短信网关向输入的手机发送验证码

  • 要求用户输入短信中的验证码是一个6位的随机数,不是字符串,因为输入的问题
  • 验证码需要有一个时间限制

针对以上需求引入非关系型数据库Redis

Redis

redis是一种支持Key-Value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景

  • Redis数据库使用ANSI C语言编写,支持网络,提供字符串,哈希,列表,队列,集合结构5种常见类型直接存取,基于内存,可持久化
  • 命令参考http://doc.yonyoucloud.com/doc/redis/index.html
  • Redis常见支持五种数据类:string字符串,hash哈希,list列表,set集合和zset即sorted set有序集合
  • redis的应用场景有:最常用会话缓存、消息队列、活动排行榜或计数、用于消息通知发布/订阅消息、列表详情内容缓存
    数据类型
    redis支持丰富的数据类型,目前可以支持9种,不同的场景使用合适的数据类型可以有效的优化内存数据的存放空间
  • string最基本的数据类型,二进制安全的字符串,最大512M
  • list按照添加顺序保持顺序的字符串列表
  • set无序的字符串集合,不存在重复的元素
  • sorted set已排序的字符串集合
  • hash是key-value对的一种集合
  • bitmap更细化的一种操作,以bit为单位。实际上底层也是通过对字符串的操作来实现
  • hyperloglog是基于概率的数据结构,近似统计大量去重元素数量的算法
  • 在2.8.9新增Geo地理位置信息储存起来, 并对这些信息进行操作。是3.2新增
  • 流Stream是5.0新增,支持多播的可持久化的消息队列,用于实现发布订阅功能
    Redis特性
  • 性能极高。Redis读的速度是110000次/s,写的速度是81000次/s
  • 丰富的数据类型。Redis支持二进制Strings、Lists、Hashes、Sets及Ordered Sets数据类型操作
  • 原子性操作。Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来
  • 丰富的特性。Redis还支持publish/subscribe、通知、key过期等特性。
    Redis安装
    Redis支持在Linux、OS X 、BSD等POSIX系统中安装,也可以在Windows中安装。redis的windows版本现在已经不更新了
    安装完成后在bin目录下还有一些可执行程序
  • redis-benchmark是Redis性能测试工具,可使用 redis-benchmark进行基准测试,如redisbenchmark -q -n 100000
  • redis-check-rdb是RDB文件检查工具,rdb是一种持久化方式
  • redis-check-aof是AOF文件检查工具,aof是一种持久化方式
  • redis-sentinel就是一个独立运行的进程,用于监控多个 master-slave 集群, 自动发现master宕机,进行自动切换 slave > master
  • redis-cli和redis-server是登录 redis 的指令和启动redis 的后台服务器的命令

通过 redis-cli 向 Redis 发送命令的方式有两种

  • 将命令作为 redis-cli 的参数执行,如关闭服务器命令redis-cli SHUTDOWN
  • redis-cli在执行时会按照默认的主机和端口号来进行连接,默认的主机和端口号分别是 127.0.0.1和6379
    连接 Redis后再使用命令操作数据库
C:\Users\Administrator>redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

Redis数据库
Redis 是一个字典结构的存储服务器,实际上一个 Redis 实例提供了多个用来存储数据的字典,客户端可以指定将数据存放在哪个字典中

  • 每个数据库对外都是以一个从 0 开始的递增数字命名,Redis默认支持16个数据库,可以通过配置databases来修改这一数字
  • 客户端与Redis建立连接后会默认选择0号数据库,不过可以使用select命令切换数据库。select 1 这条指令就会选择 1 号数据库

Redis数据库与MySQL数据库不同

  • 首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据
  • 另外Redis也不支持为每个不同的数据库设置不同的访问密码。
  • 最重要的是多个数据库之间并没有完全隔离,如FLUSHALL命令会清除所有数据库中的数据。
    string数据类型
  • string是redis最基本的类型,而且string类型是二进制安全的。意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象
  • String类型是最基本的数据类型,一个redis中字符串value最多可以是512M
  • redis底层提供了三种不同的数据结构实现字符串对象,根据不同的数据自动选择合适的数据结构。这里的字符串对象并不是指的纯粹的字符串,数字也是可以的 【面试】
    • int当数据是long类型的整数字符串时,底层使用long类型的整数实现。这个值会直接存储在字符串对象的ptr属性中,同时OBJECT ENCODING为int。对应的加减操作命令为incr和decr
    • raw当数据为长度大于44字节的字符串时,底层使用简单动态字符串实现,redis的简单随机字符串SDS有三个属性,free,len和buf。free存的是还剩多少空间,len存的是目前字符串长度,不包含结尾的空字符。buf是一个list,存放真实字符串数据,包含free和空字符
    • embstr当数据为长度小于44字节的字符串时,底层使用embstr编码的简单动态字符串实现。相比于raw,embstr内存分配只需要一次就可完成,分配的是一块连续的内存空间
  • 当保存的数据中包含字符时,String类型就会用简单动态字符串SDS结构体来保存
    • redis中的String并不是以\0结束,而是根据len来决定文件是否结束。其主要目的是为了存储非String的数据,例如图片,影像等二进制数据
    • 由于String结构体中存储了长度,所以在获得长度的时间复杂度为O(1)
    • String在扩容的时候,如果数据的长度小于10MB则,每次扩容为之前以后数据的2倍。如果超过10MB则每次只增加1MB的长度
  • 主要应用场景:1、存储数据。如常见存储 K-V 字符串、JSON字符串。2、程序计数 INCR 命令递增或递增一个数。3、分布式锁,使用 SET key value NX ,NX 不存在才写入。4、单点登录。可作为存储共享会话实现单点登录。
    基本命令行操作
    SET key value [EX seconds] [PX milliseconds] [NX|XX] 将字符串值value关联到key。如果key已经持有
    其他值, SET就覆写旧值,无视类型。对于某个原本带有生存时间TTL的键来说, 当SET命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
  • NX只在键不存在时,才对键进行设置操作。 SET key value NX效果等同于SETNX key value
  • XX只在键已经存在时,才对键进行设置操作
    GET key返回key所关联的字符串值。如果key不存在那么返回特殊值nil。假如key储存的值不是字符串类型,返回一个错误,因为GET只能用于处理字符串值。
    getset key value设置此key的value为新输入的值,同时返回老的值,把一次get和一次set两次IO的请求变为一次

EXPIRE key seconds为给定key设置生存时间,当key 过期时,生存时间为0,它会被自动删除。

在 Redis 2.4 版本中,过期时间的延迟在 1 秒钟之内 —— 也即是,就算 key 已经过期,但它还是可能在过期之后一秒钟之内被访问到,而在新的 Redis 2.6 版本中,延迟被降低到 1 毫秒之内。

数值使用场景:秒杀、点赞、评论
可以规避并发下,对数据库的事务操作,完全由Redis内存操作代替
Hash数据类型
hash类型实际上是以key为标识存储key-value对。redis hash是一个string类型的field和value的映射表,添加,删除操作都是平均O(1),hash特别适合用于存储对象

  • 有压缩链表ziplist和字典结构hashtable两种底层实现
    • hashtable采用链地址法解决hash冲突问题
    • 当hash对象可以同时满足以下两个条件时,哈希对象使用ziplist编码,超过限制则使用hashtable结构存储数据
  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

ziplist编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时,会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩 列表表尾。
因此保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被 放在压缩列表的表尾方向
压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的 内存区域,目的是节省内存

  • 优点: 内存地址连续,省去了每个元素的头尾节点指针占用的内存
  • 缺点: 对于删除和插入操作比较可能会触发连锁更新反应,比如在list中间插入删除一个元素 时,在插入或删除位置后面的元素可能都需要发生相应的移位操作。

hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键
  • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值

Hash类型两种编码方式,ziplist 与 hashtable。

  • hashtable 编码的哈希对象使用字典作为底层实现
    ziplist 与 hashtable 编码方式之间存在单向转换
  • 既想节省Redis内存空间,又想存储对象数据,又想访问速度快,hash似乎是不二的选择
    List数据类型
    list是redis的一种基础数据结构,内部使用双向链表quicklist实现,所以向列表两端添加元素的时间复杂
    度为O(1), 获取越接近两端的元素速度就越快
    redis中的列表对象经常被用作消息队列使用,底层有双向链表linkedList、支持双向遍历的压缩列表
    zipList和quickList三种存储方式
  • 在Redis3.2版本后,引入的qucikList是zipList和双向链表linkedList组成的混合体
    可以作链表使用

双向链表linkedList链表是双端结构,listNode结构体中带有prev和next指针,因此获取某个节点的前置节点和后继节点的时间复杂度都是O(1)

  • 这个链表无环:表头节点的prev和表尾节点的next指针都指向了NULL,对链表的访问以NULL为终点
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链接的表头节点和表尾节点的时间复杂度为O(1)
  • 带有链表长度计数器:程序使用list结构体的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)
  • 多态:链表节点使用void*指针来保存节点值,可以用于保存不同类型的值

ziplist和linkedlist

因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现
试图往列表新添加一个字符串值,且这个字符串的长度超过默认值64或者ziplist 包含的节点超过默认值为 512
ziplist是一个特殊的双向链表。没有维护双向指针prev、next,而是存储上一个entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。这是牺牲读取的性能,获得高效的存储空间,因为简短字符串存储指针比存储entry长度更费内存。这是典型的时间换空间

Redis中的列表list,在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的,但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。

双向链表linkedlist便于在表的两端进行push和pop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
quickList是ziplist和linkedlist二者的结合,是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。本质上来说,quicklist里面保存着一个一个小的ziplist
quickList就是一个标准的双向链表的配置,有head有tail;每个节点是一个quicklistNode,包含prev和next指针。每一个quicklistNode包含一个ziplist,压缩链表里存储键值。所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

应用场景:

  • 消息队列:列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列
  • 文章列表:对于博客站点来说,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。

Set集合数据类型
Set类型的value保证每个值都不重复
redis中的集合对象底层有两种实现方式,分别有整数集合intset和hashtable。当所有元素都是整数且元素数小于512时采用整数集合实现,其余情况都采用hashtable实现
整数集合intset有三个属性,encoding记录数字的类型,有int16,int32和int64等,length记录集合的长度content存储具体数据

  • 创建set的时候如果遇上成员是整形字符串时,会直接使用intset编码存储。intset的content区域是整数数组的一个有序集合,其内部是采用二分查找来定位元素位置
  • hashtable的key为set中元素的值,而value为null
    应用场景:
  • 微博关注我的人和我关注的人都适合用集合存储,可以保证人员不会重复
  • 中奖人信息也适合用集合类型存储,这样可以保证一个人不会重复中奖。

Zset有序集合
有序集合对象zset和集合对象set没有很大区别,仅仅是多了一个分数score用来排序

  • redis中的有序集合底层采用ziplist和skiplist跳表实现,当所有字符串长度都小于设定值值64字节,并且所存元素数量小于设定值512个使用ziplist实现,其他情况均使用skiplist实现

  • 当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值

  • 有序集合的底层实现之一是跳表, 除此之外跳表它在 Redis 中没有其他应用

  • 整数集合intset是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现

  • 数据少时使用ziplist压缩列表,占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。ziplist为了节省内存,每个元素占用的空间可以不同,对于大数据(long long),就多用一些字节存储,而对于小的数据(short),就少用一些字节来存储。因此查找的时候需要按顺序遍历。ziplist省内存但是查找效率低

  • 跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的,具有以下性质:

    • 有很多层结构组成
    • 每一层都是一个有序的链表,排列顺序为由高到低,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点
    • 最底层的链表包含了所有的元素
    • 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全部都会出现
    • 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点
      在这里插入图片描述
  • 搜索,从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也及时和当前层的下一层的节点下一个节点

  • 插入,首先确定插入的层数,有一种方法是抛一个硬币,如果是正面就累加,直到遇到反面为止,最后记录正面的次数作为插入的层数,当确定插入的层数K后,则需要将新元素插入从底层到K层

  • 删除,在各个层中找到包含指定值得节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层

Redis缓存

问题1:缓存雪崩
一般使用缓存用于缓冲对DB的冲击,如果缓存宕机,所有请求将直接打在DB,造成DB宕机从而导致整
个系统宕机
2 种策略(同时使用):

  • 对缓存做高可用HA,防止缓存宕机
  • 使用断路器,如果缓存宕机,为了防止系统全部宕机,限制部分流量进入 DB,保证部分可用,其余的请求返回断路器的默认值。
    大量的热点数据同时到期,解决方案为过期时间上引入随机数,以防止同时到期的问题
    问题2:缓存穿透
    解释 1: 缓存查询一个没有的 key,同时数据库也没有,如果黑客大量的使用这种方式,那么就会导致
    DB 宕机
    解决方案:可以使用一个默认值来防止,例如,当访问一个不存在的 key,然后再去访问数据库,还是
    没有,那么就在缓存里放一个占位符,下次来的时候,检查这个占位符,如果发生时占位符,就不去数
    据库查询了,防止 DB 宕机。
    解释 2: 大量请求查询一个刚刚失效的 key,导致 DB 压力倍增,可能导致宕机,但实际上,查询的都
    是相同的数据。
    解决方案: 可以在这些请求代码加上双重检查锁。但是那个阶段的请求会变慢。不过总比 DB 宕机好。
    问题3:缓存击穿
    缓存击穿是指缓存中没有但数据库中有的数据。这时由于并发用户特别多,同时读缓存没读到数据,又
    同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
    解决方案:
    设置热点数据永远不过期
    缓存预热
    问题4:缓存并发竞争
    解释: 多个客户端写一个 key,如果顺序错了,数据就不对了。但是顺序无法控制。
    解决方案: 使用分布式锁,例如 zookeeper,同时加入数据的时间戳。同一时刻,只有抢到锁的客户端
    才能写入,同时,写入时,比较当前数据的时间戳和缓存中数据的时间戳
    问题5:缓存和数据库双写不一致 【面试】
    解释:连续写数据库和缓存,但是操作期间,出现并发了,数据不一致了。
    通常更新缓存和数据库有以下几种顺序:
  • 先更新数据库,再更新缓存
  • 先删缓存,再更新数据库
  • 先更新数据库,再删除缓存
  • 先更新数据库,再更新缓存。
    问题:当有 2 个请求同时更新数据,那么如果不使用分布式锁,将无法控制最后缓存的值到底是多少。
    也就是并发写的时候有问题。
    先删缓存,再更新数据库。
    问题:如果在删除缓存后,有客户端读数据,将可能读到旧数据,并有可能设置到缓存中,导致缓存中的数据一直是老数据。
    有 2 种解决方案:
  • 使用双删,即删更删,最后一步的删除作为异步操作,就是防止有客户端读取的时候设置了旧值
  • 使用队列,当这个key不存在时,将其放入队列,串行执行,必须等到更新数据库完毕才能读取数据。
    先更新数据库,再删除缓存
    常用的方案叫 Cache Aside Pattern,如果先更新数据库,再删除缓存,那么就会出现更新数据库之前有
    瞬间数据不是很及时。同时,如果在更新之前,缓存刚好失效了,读客户端有可能读到旧值,然后在写
    客户端删除结束后再次设置了旧值,非常巧合的情况。
    有 2 个前提条件:缓存在写之前的时候失效,同时,在写客户度删除操作结束后,放置旧数据 —— 也就
    是读比写慢。 设置有的写操作还会锁表。所以,这个很难出现,但是如果出现了则使用双删。记录更新
    期间有没有客户端读数据库,如果有,在更新完数据库之后,执行延迟删除。
    还有一种可能,如果执行更新数据库,准备执行删除缓存时,服务挂了,执行删除失败这就麻烦了。不
    过可以通过订阅数据库的 binlog 来删除。

Spring Data Redis

spring-data-redis是spring-data模块的一部分,专门用来支持在spring管理项目对redis的操作

Redis客户端

基于java语言的redis客户端就是集成了redis的命令操作

  • redis-cli是redis官方提供的客户端,可以看作一个shell程序,它可以发送命令对redis进行操作。
  • 例如jedis是使用java语言操作redis,双方都遵循redis提供的协议,按照协议开发对应的客户端。
    Springboot2之前redis的连接池为jedis,2.0以后redis的连接池改为了lettuce,lettuce能够支持高版本的redis、需要java8及以上。Lettuce是基于netty实现的与redis进行同步和异步的通信。
    lettuce和jedis比较
    jedis使直接连接redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个jedis实例增加物理连接 ; lettuce的连接是基于Netty的,连接实例StatefulRedisConnection可以在多个线程间并发访问,StatefulRedisConnection是线程安全的,所以一个连接实例可以满足多线程环境下的并发访问,当然这也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
  • jedis客户端连接方式是基于tcp的阻塞式连接方式。
  • lettuce客户端连接方式是基于netty的多路复用异步非阻塞的连接方案。目前业界解决高并发大数据的问题的思路
    Jedis是Redis的Java实现客户端,提供了比较全面的Redis命令的支持
    优点:
  • 提供了比较全面的 Redis 操作特性的 API
  • API 基本与 Redis 的指令一一对应,使用简单易理解
    缺点:
  • 同步阻塞 IO
  • 不支持异步
  • 线程不安全
    Lettuce高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
    优点:
  • 线程安全
  • 基于 Netty 框架的事件驱动的通信,可异步调用
  • 适用于分布式缓存
    缺点:
  • API 更抽象,学习使用成本高
    客户端选择
    调大连接池大小能够提高jedis的吞吐量,但是不能避免出现超时错误和长时间等待。jedis连接方式最大连接数和最小、最大空闲连接数设置为一样有利于减少上下文切换时间,提升效率。
    lettuce调大连接池大小反而会影响性能,最佳个数=CPU核数+1,lettuce整体稳定性和性能优于jedis方式。
    开发步骤
    redis应用开发有2种不同的方法
  • 通过RedisTemplate自行编程实现缓存的管理,灵活性高
  • 通过Spring cache提供的注解使用,无需编程使用RedisTemplate编程方式管理缓存
    1、添加依赖spring-boot-starter-data-redis和commons-pool2以及jackson-databind
    2、添加redis相关的配置信息
spring:
	redis:
		host: localhost
		port: 6379
		lettuce:
			pool:
			enabled: true 需要使用连接池,如果使用连接池则需要引入commons-pool2依赖
			max-active: 5 最大活跃连接数
			min-idle: 5 最小空闲连接数
			max-wait: 5000 最大等待时间,默认-1表示无限制等待,单位ms

3、添加配置类,主要用于定义所使用的序列化器

@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable>
redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new
RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new
GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

RedisTemplate用法

  • redisTemplate.hasKey(key)判断是否有key所对应的值,有则返回true,没有则返回false
  • redisTemplate.opsForValue().get(key)有则取出key值所对应的值

大量api进行了归类封装,将同一类型操作封装为operation接口
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
redisTemplate.delete(key) 删除单个key值
redisTemplate.delete(keys) 删除集合中的所有key对应的值,其中keys: Collection

设置过期时间

public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}

查找匹配的key值,返回一个Set集合类型

public Set<String> getPatternKey(String pattern) {
return redisTemplate.keys(pattern);
}

如果旧key值存在时,将旧key值改为新key值

redisTemplate.rename("bbb","123:999"); //返回值void
redisTemplate.rename("eee","ffff"); //如果oldkey不存在则
RedisCommandExecutionException: ERR no such key

有条件修改

public Boolean renameOldKeyIfAbsent(String oldKey, String newKey) {
return redisTemplate.renameIfAbsent(oldKey, newKey); //如果oldkey不存在则
RedisCommandExecutionException
}

返回当前key所对应的剩余过期时间

public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}

如果返回-1无限制,返回-2对应的key值不存在
返回剩余过期时间并且指定时间单位

public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}

将key持久化保存

public Boolean persistKey(String key) {
return redisTemplate.persist(key);
}

设置当前的key以及value值 redisTemplate.opsForValue().set(key, value)
设置当前的key以及value值并且设置过期时间 redisTemplate.opsForValue().set(key, value,
timeout, unit)
将旧的key设置为value,并且返回旧的key对应的value。如果key不存在则执行set操作,返回为null

public String setKeyAsValue(String key, String value) {
return redisTemplate.opsForValue().getAndSet(key, value);
}

修改key对应的value值,执行加操作,返回新值。要求key对应的value值应该可以转换为int类型,否则报错

public Long incrBy(String key, long increment) {
return redisTemplate.opsForValue().increment(key, increment);
}

redisTemplate.opsForValue().size(key) 获取字符串的长度
重新设置key对应的值,如果存在返回false,否则返回true
redisTemplate.opsForValue().setIfAbsent(key, value)
将值 value 关联到 key,并将 key 的过期时间设为 timeout
redisTemplate.opsForValue().set(key, value, timeout, unit)

序列化机制

redistemplate 默认是把键和值序列化之后再存储的;取值的时候再反序列化把正常的值交还给用户
操作对象的问题
类定义

@Data
public class User implements Serializable { //必须实现序列化接口
private Long id;
private String username;
private Date birth;
}

调用redisTemplate存储一个User类型的对象

User tmp=new User();
tmp.setId(99L);
tmp.setUsername("白澄湖");
tmp.setBirth(new Date());
redisTemplate.opsForValue().set("users::99",tmp);
User user=(User) redisTemplate.opsForValue().get("users::99");
//User(id=99, username=白澄湖, birth=Wed Feb 01 17:27:18 CST 2023)
System.out.println(user);

直接查看redis中的数据库内容可以发现实际上存储的是一个JSON格式的字符串
spring 中预定义的序列化方案有

  • JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略
  • StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和"tring.getBytes(charset)”的直接封装。是最轻量级和
    高效的策略
  • GenericJackson2JsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-databind工具支持】

需要通过配置类设置所使用的序列化器

@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable>
redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new
RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new
GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

面试问题
如果从Redis的核心网络模型来看,从 Redis 的 v1.0 到 v6.0 版本之前,Redis 的核心网络模型一直是一
个典型的单 Reactor 模型:利用 epoll/select/kqueue 等多路复用技术,在单线程的事件循环中不断去处理事件(客户端请求),最后回写响应数据到客户端。这个单线程网络模型一直到 Redis v6.0 才改造成多线程模式。但是从Redis整个数据库服务器而言,这并不意味着整个 Redis 一直都只是单线程。

为什么Redis是单线程的,但是运行速度反而还快呢?

  • Redis是基于内存的,内存的读写速度非常快
  • redis是单线程的,省去了很多上下文切换线程的时间
  • redis使用多路复用技术,可以处理并发的连接;

上下文切换就是cpu在多线程之间进行轮流执行,抢占cpu资源,而redis单线程的,因此避免了繁琐的多线程上下文切换。

多路复用:

多路指的是多个socket连接,复用指的是复用一个线程。 目前多路复用主要有三种技术:select,poll,epoll。

  • 举个例子:一个酒吧服务员,前面有很多醉汉,epoll这种方式相当于一个醉汉吼了一声要酒,服务员听见之后就去给他倒酒,而在这些醉汉没有要求的时候可以玩玩手机等。但是select和poll技术是这样的场景:服务员轮流着问各个醉汉要不要倒酒,没有空闲的时间。io多路复用的意思就是多个醉汉公用一个服务员。
  • select:
    1.会修改传入的参数,对于多个调用的函数来说非常不友好;
    2.要是sock(io流出现了数据),select只能轮询这去找数据,对于大量的sock来说开销很大;
    3.不是线程安全的
    4.只能监视1024个连接;
  • poll:
    1.还不是线程安全的…
    2.去掉了1024个连接的限制;
    3.不修改传入的参数了;
  • epoll:
    1.线程安全了;
    2.epoll不仅能告诉你sock有数据,还能告诉你哪个sock有数据,不用轮询了;
    3.但是只支持linux系统;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/193074.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信公众号小程序怎么做?

​微信公众号小程序在当下已经成为人们日常生活中不可或缺的工具&#xff0c;在用户体验方面也做得很好&#xff0c;不仅可以实现沟通和交流&#xff0c;还可以通过微信公众号进行在线预约服务。那么关于微信公众号小程序怎么做&#xff0c;下面就给大家说说。 1、注册微信公众…

Cadence PCB仿真 使用 Allegro PCB SI 元器件端口设置的PDN分析功能介绍图文教程

🏡《总目录》   🏡《分目录》 目录 1,概述2,启动方法3,功能介绍3.1,元器件设置列表(Device)3.2,端口设置列表(Ports)4,总结1,概述 在进行PDN分析时需要对电源网络涉及到的所有元器件的指定端口的参数进行配置。本文介绍PDN网络元器件端口设置的功能。 2,启动…

【寒假day3】leetcode刷题

&#x1f308;一、选择题 ❤第1题&#xff1a;关于重载函数,&#xff08; &#xff09;说明是正确的。 A: 函数名相同&#xff0c;参数类型或个数不同 B: 函数名相同&#xff0c;返回值类型不同 C: 函数名相同&#xff0c;函数内部实现不同 D: 函数名称不同答案&#xff1a…

数据挖掘-特征选择方法:方差过滤,相关性过滤

目录特征选择1、Filter过滤法方差过滤1&#xff0c;消除方差为0的特征2&#xff0c;只留下一半的特征3&#xff0c;特征是二分类时2、相关性过滤法2.1 卡方过滤2.2 F检验2.3 互信息法3、 Embedded嵌入法4、Wrapper包装法5、总结特征选择 数据预处理完成后&#xff0c;就进入特…

常见网络报文数据包格式

当我们应用程序用TCP传输数据的时候&#xff0c;数据被送入协议栈中&#xff0c;然后逐个通过每一层&#xff0c;知道最后到物理层数据转换成比特流&#xff0c;送入网络。而再这个过程中&#xff0c;每一层都会对要发送的数据加一些首部信息。整个过程如下图。以太网帧格式以太…

Mybatis框架(二)再遇Mybatis之Mybatis配置文件与映射文件

本文是本人专栏【Java开发后端系列框架】里的文章&#xff0c;文章根据各框架官网与网上资料加上本人工作经验&#xff0c;进行修改总结发布在这个专栏&#xff0c;主要目的是用于自我提升&#xff0c;不用于获取利益。如果系列文章能到帮到您本人将感到荣幸&#xff0c;如果有…

2023年3月5日DAMA-CDGA/CDGP数据治理认证考试报名入口

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

【每日阅读】前端进阶知识点(一)

如何更改网页中语言属性值 声明当前语言类 html标签更改属性值 lang属性中不区分大小写 en-us en-US 一致 具体可使用 window,document.querySelector(“html”)?.setAttribute(“lang”,newValue); qs库 qs是一个流行的查询参数序列化和解析库。可以将一个普通的object序列…

OpenPPL PPQ量化(2):离线静态量化源码剖析

目录 模型支持 量化onnx原生模型&#xff1a;quantize_onnx_model 输入输出 执行流程 ONNX格式解析 后记 模型支持 openppl支持了三种模型&#xff1a;onnx、caffe、pytorch&#xff0c;其中pytorch和caffe是通过quantize_torch_model和quantize_caffe_model&#xff0c…

Elasticsearch:Terms set 查询

什么是 terms set 查询&#xff1f; Terms set 查询根据匹配给定字段的精确术语的最少数量返回文档。 terms set 查询与 term 查询有何不同&#xff1f; Terms set query 和 Terms query 之间的唯一区别是你可以提供必须匹配的最少数量的术语才能检索特定文档。 什么是 minim…

【Ansys Meshing】Fluent Meshing和Ansys Meshing在划分边界层网格能力上的对比

一、几何模型展示 如下图所示&#xff0c;一端的圆柱是流体入口&#xff0c;另一端的圆柱是流体出口&#xff0c;中间都是导热管。 二、在spaceclaim中进行切割实体 2.1 切割手段 切割平面的位置如图两根线所示&#xff0c;最终得到左右两边两个有圆柱的大块&#xff0c;以…

【SAP Abap】X档案:SAP 快速提供基础数据给第三方系统访问的几种方法(附常用基础数据)

SAP 快速提供基础数据给第三方系统访问的几种方法1、数据封装2、开放RFC访问3、开放接口服务4、开放DB访问5、常用基础数据1、数据封装 在企业信息系统建设过程中&#xff0c;少不了的就是系统集成数据对接。 尤其是SAP系统中大量的基础数据集成&#xff0c;如各种字段值域&am…

C语言:操作符详解

往期文章 C语言&#xff1a;初识C语言C语言&#xff1a;分支语句和循环语句C语言&#xff1a;函数C语言&#xff1a;数组 目录往期文章前言1. 操作符分类2. 算术操作符3. 移位操作符4. 位操作符5. 赋值操作符6. 符合赋值符7. 单目操作符8. 关系操作符9. 逻辑操作符10. 条件操作…

Java 中的基本设计模式

设计模式是针对常见软件设计问题的可重用解决方案。它们提供了一种以一致且高效的方式组织和构建代码的方法。一些常见的设计模式包括&#xff1a;工厂模式是一种创建型设计模式&#xff0c;它提供用于在超类中创建对象的接口&#xff0c;但允许子类更改将要创建的对象的类型。…

【数据结构趣味多】优先级队列——堆

1. 优先级队列 概念&#xff1a; 队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比…

前端打包后生成的dist 或 build目录,如何在本地启动服务运行

前端打包后生成的dist/build目录&#xff0c;如何在本地启动服务运行 运行npn run build&#xff0c;会打包后会产生 dist 或 build 目录 一般情况下&#xff0c;直接打开dist 或 build 目录下的 index.html会在浏览器看到内容。 然而发现网页一片空白&#xff0c;打开了控制台…

C++基础入门(引用补充)

1、使用场景做参数void Swap(int& left, int& right) {int temp left;left right;right temp; }做返回值int& Count() {static int n 0;n;// ...return n; }c语言内&#xff0c;出了count函数&#xff0c;n被销毁&#xff0c;会创建临时变量存储其值&#xff0…

DETR——使用Transformer进行端到端目标检测的开端之作

深度学习知识点总结 专栏链接: https://blog.csdn.net/qq_39707285/article/details/124005405 此专栏主要总结深度学习中的知识点&#xff0c;从各大数据集比赛开始&#xff0c;介绍历年冠军算法&#xff1b;同时总结深度学习中重要的知识点&#xff0c;包括损失函数、优化器…

寻根究底,为什么Docker中的Alpine Linux镜像能这么小

去年我发表了文章对Docker基础镜像的思考&#xff0c;该不该选择alpine&#xff0c;其中对于Alpine Linux镜像如此之小的原因我解释为它使用了musl而不是glibc 有人发现并指出了我的这个错误&#xff0c;说musl与glibc的大小差别不足以造成如此大的差距&#xff0c;应该别有原…

C++-静态局部变量

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 特征&#xff1a; 1.需添加关键字static。 2.在静态存储区分配内存&#xff0c;始终存在直到程序结束。 3.作用域为局部&#…