RBAC简介

2023-11-14

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系统;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

RBAC简介 的相关文章

  • “修改列”与“更改列”

    我知道 我们不能使用重命名列MODIFY COLUMN语法 但我们可以使用CHANGE COLUMN syntax 我的问题是 主要用途是什么modify syntax 例如 ALATER TABLE tablename CHANGE co
  • IntelliJ IDEA 创建的 JAR 文件无法运行

    我在 IntelliJ 中编写了一个跨越几个类的程序 当我在 IDE 中测试它时它运行良好 但是 每当我按照教程将项目制作成 jar 可执行文件时 它就不会运行 双击 out 文件夹中的文件时 该文件不会运行 并显示 无法启动 Java J
  • Convert.FromBase64String 方法的 Java 等效项

    Java 中是否有相当于Convert FromBase64String http msdn microsoft com en us library system convert frombase64string aspx which 将指
  • HDFS:使用 Java / Scala API 移动多个文件

    我需要使用 Java Scala 程序移动 HDFS 中对应于给定正则表达式的多个文件 例如 我必须移动所有名称为 xml从文件夹a到文件夹b 使用 shell 命令我可以使用以下命令 bin hdfs dfs mv a xml b 我可以
  • 如何在jsp代码中导入java库?

    我有以下jsp代码 我想添加 java io 等库 我怎样才能做到这一点
  • 使用替换字符串中多个单词的最有效方法[重复]

    这个问题在这里已经有答案了 此刻我正在做 Example line replaceAll replaceAll cat dog replaceAll football rugby 我觉得那很丑 不确定有更好的方法吗 也许循环遍历哈希图 ED
  • OnClick 事件中的 finish() 如何工作?

    我有一个Activity一键退出Activity 通过layout xml我必须设置OnClick事件至cmd exit调用 this finish 效果很好 public void cmd exit View editLayout thi
  • 无法理解 Java 地图条目集

    我正在看一个 java 刽子手游戏 https github com leleah EvilHangman blob master EvilHangman java https github com leleah EvilHangman b
  • 在具有相同属性名称的不同数据类型上使用 ModelMapper

    我有两节课说Animal AnimalDto我想用ModelMapper将 Entity 转换为 DTO 反之亦然 但是对于具有相似名称的一些属性 这些类应该具有不同的数据类型 我该如何实现这一目标 动物 java public class
  • 如何将文件透明地传输到浏览器?

    受控环境 IE8 IIS 7 ColdFusion 当从 IE 发出指向媒体文件 例如 mp3 mpeg 等 的 GET 请求时 浏览器将启动关联的应用程序 Window Media Player 我猜测 IIS 提供文件的方式允许应用程序
  • 从 android 简单上传到 S3

    我在网上搜索了从 android 上传简单文件到 s3 的方法 但找不到任何有效的方法 我认为这是因为缺乏具体步骤 1 https mobile awsblog com post Tx1V588RKX5XPQB TransferManage
  • Spring Data 与 Spring Data JPA 与 JdbcTemplate

    我有信心Spring Data and Spring Data JPA指的是相同的 但后来我在 youtube 上观看了一个关于他正在使用JdbcTemplate在那篇教程中 所以我在那里感到困惑 我想澄清一下两者之间有什么区别Spring
  • 将 Long 转换为 DateTime 从 C# 日期到 Java 日期

    我一直尝试用Java读取二进制文件 而二进制文件是用C 编写的 其中一些数据包含日期时间数据 当 DateTime 数据写入文件 以二进制形式 时 它使用DateTime ToBinary on C 为了读取 DateTime 数据 它将首
  • Java中未绑定通配符泛型的用途和要点是什么?

    我不明白未绑定通配符泛型有什么用 具有上限的绑定通配符泛型 stuff for Object item stuff System out println item Since PrintStream println 可以处理所有引用类型 通
  • 将 JSON 参数从 java 发布到 sinatra 服务

    我有一个 Android 应用程序发布到我的 sinatra 服务 早些时候 我无法读取 sinatra 服务上的参数 但是 在我将内容类型设置为 x www form urlencoded 之后 我能够看到参数 但不完全是我想要的 我在
  • Springs 元素“beans”不能具有字符 [children],因为该类型的内容类型是仅元素

    我在 stackoverflow 中搜索了一些页面来解决这个问题 确实遵循了一些正确的答案 但不起作用 我是春天的新人 对不起 这是我的调度程序 servlet
  • android Accessibility-service 突然停止触发事件

    我有一个 AccessibilityService 工作正常 但由于开发过程中的某些原因它停止工作 我似乎找不到这个原因 请看一下我的代码并告诉我为什么它不起作用 public class MyServicee extends Access
  • 将2-3-4树转换为红黑树

    我正在尝试将 2 3 4 树转换为 java 中的红黑树 但我无法弄清楚它 我将这两个基本类编写如下 以使问题简单明了 但不知道从这里到哪里去 public class TwoThreeFour
  • java迭代器内部是如何工作的? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我有一个员工列表 List
  • 中断连接套接字

    我有一个 GUI 其中包含要连接的服务器列表 如果用户单击服务器 则会连接到该服务器 如果用户单击第二个服务器 它将断开第一个服务器的连接并连接到第二个服务器 每个新连接都在一个新线程中运行 以便程序可以执行其他任务 但是 如果用户在第一个

随机推荐

  • 校园网服务器系统需求分析,校园网需求及分析.doc

    校园网需求及分析 校园网络规划与设计 一 毕业设计课题名称 校园网络规划与设计 二 毕业设计任务 1 需求分析 2 系统设计原则和实现目标 1 网络系统设计原则 系统建设目标 2 网络性能分析 3 系统的总体设计 1 网络拓扑结构设计 2
  • 计算机主板上的fan,通用解决方案:计算机主板上的CPU_FAN,SYS_FAN,CHA_FAN,CPU_OPT接口知识...

    在组装计算机的过程中 尽管安装过程很简单 但经常会遇到接线问题 用户经常错误地将CPU散热器的电源线插入SYS FAN 尽管风扇可以旋转 但是在引导时可能会出现F1错误 CPU Fan Error 这也会导致CPU散热器无法智能调节速度 让
  • javafx开发- ImageView标签设置图片不显示

    图片不显示的原因没找到 但是找到了解决办法 嘿嘿 解决问题代码如下
  • flutter之Provider(一)

    网上有不少介绍Provider的文章 但是感觉大部分对于初学者而言不够友好 很多在文章开始就写了大片的代码或者是一通的状态管理的介绍 但是实际上根本不需要那么复杂 本篇文章当然也会简单的介绍Provider的使用 但是我们更多还是通俗的介绍
  • 新手教程!设置PDF文件的页面大小

    设置文档的页面大小是办公一族经常遇到的一种操作 如果是word文档 那简直就是so easy 但在日常工作中 我们偶尔会遇到PDF格式的文件 由于对它不熟悉 想要对PDF文档的页面大小进行修改 又该如何操作呢 这时我们就需要借助一款非常专业
  • 客户价值预测:线性回归模型与诊断(概念)

    客户生命周期可分为四个阶段 潜在客户阶段 响应客户阶段 既得客户阶段 流失客户阶段 本章整体是一个客户价值预测的案例 背景是某信用卡公司在地推活动之后 获取了大量客户的信用卡申请信息 其中一个部分客户顺利开卡 并且有月消费记录 而另外一部分
  • C语言结构体应用-通讯录

    这里写目录标题 总体介绍 一 数据的定义及数据初始化 二 增加联系人 三 删除联系人 四 修改某个联系人 五 显示所有联系人 六 删除所有联系人 七 按名字首字母排序联系人 八 查找联系人 九 代码展示 总体介绍 本文主要介绍一个结构体的应
  • vue 点击事件失效

    点击事件失效的情况 总共有三种 1 没有点到那个元素 比如说div gt span 事件绑定在div上 但是它可能点来点去是在span标签上面 这种情况 把 click点击事件绑定到span上测试一下就好了 如果是被覆盖了 加个这个 sto
  • error while loading shared libraries: libcublasLt.so.11 解决方法

    在运行cuda程序的时候 有时候会遇到此类错误 error while loading shared libraries libcublasLt so 11 问题是两个 确实没有此类库文件 有此库文件 不过没有放在正确的地方 针对第一类 如
  • Java实现将JSON文件导出到Excel

    文章目录 一 运行环境 二 需求描述 三 实现思路 四 实现代码 一 运行环境 windows10 IDEA 2022 JDK 8 Maven 3 8 6 Apache POI 5 fastjson2 二 需求描述 写一个功能 任意json
  • 利用Vulnhub复现漏洞 - Adobe ColdFusion 反序列化漏洞(CVE-2017-3066)

    Adobe ColdFusion 反序列化漏洞 CVE 2017 3066 Vulnhub官方复现教程 漏洞原理 复现漏洞 启动环境 漏洞复现 生成POC 发送POC 发送POC内容 检验POC 进入容器 通过DockerID进入容器 查看
  • 软工实践2019——第二次作业评分

    第二次作业评分 第二次作业原文 写在前面的话 看了大家陆续提交的第一次作业 感慨良多 初心 勇气和信心 回顾初心 回想自己当初为什么报这个专业 不知你们是否看过电影 无问西东 其中有一句台词 如果提前了解了你们要面对的人生 不知你们是否还会
  • VirtualBox中安装Android-x86详解

    1 下载安装VirtualBox 官网 http www virtualbox org wiki Downloads 2 下载Android x86 官网 http www android x86 org download 这里我们下载5
  • 19. 第三方库的管理和虚拟环境

    Hi 大家好 我是茶桁 在我们之前的课程中 讲解了数据 函数 类 模块以及包 这些基本上已经构成了Python的全部了 那么 我们在学习Python的包之后 有没有思考过 既然Python有内置模块 我们也可以自己写一些模块来使用 那一定有
  • 3D游戏设计作业10:AR/MR 技术

    AR MR 技术 游戏截图 1 作业要求 1 图片识别与建模 2 虚拟按键小游戏 2 设计思路 1 首先是要安装Vuforia 这里直接在file build settings player settings里勾选Vuforia Augme
  • 【漏洞复现】CVE-2021-32682 elFinder ZIP 参数与任意命令注入

    1 Vulhub启动环境 2 查看端口号 3 输入网址 ip 8080 打开网页 4 先创建一个普通的文本文件1 txt 5 然后右键这个文件 对其进行打包 打包后的文件命名为2 zip 并同时进行抓包 获取1 txt的base64编码 6
  • 成员变量与局部变量

    一 成员变量 在类中定义 用来描述对象将要有什么 二 局部变量 在类的方法中定义 在方法中临时保存数据 三 成员变量和局部变量的区别 1 作用域不同 局部变量的作用域仅限于定义它的方法 成员变量的作用域在整个类内部都是可见的 2 初始值不同
  • 【总结】爬虫流程

    爬虫流程 根据所需数据确定爬虫网页 首先考虑resquests 需要提前导入 1 若是文本数据 用response text 2 若是下载视频 图片 音频 用response content 3 若是json接口 用response jso
  • CSS整体界面设计

  • RBAC简介

    RBAC BAC基于角色的访问控制 RBAC认为权限授权的过程可以抽象地概括为 Who是否可以对What进行How的访问操作 RBAC简介 基于角色的权限访问控制模型 在RBAC模型里面 有3个基础组成部分 分别是 用户 角色和权限 RBA