JWT +Redis
一、流程
- 前端:前端拿到登录名和密码,使用JSEncrypt实现rsa将密码进行加密,然后传到后端
- 后端:拿到密码通过后台的私钥进行解密,然后通过用户名查询到用户信息,通过用户信息的状态来判断登录结果
- 前端:若后端验证成功则根据规则生成Token,并存入redis,且像前端返回token,前端将token和用户存到localstorage,客户端再次发送请求数据将携带token。
- 后端:通过拦截器拦截请求,在请求头中拿到token,验证redis中是否由token,且是否过期,如果token不存在或者过期,则提示身份验证未通过,或者验证信息失效,佛则则请求成功。
二、 知识点
一、Token
- Token:客户端频繁的向服务器请求数据,服务器频繁的去数据库查询用户名和密码进行比对,判断用户名和密码正确与否,并作出相应的提示,这样的情况下服务器压力较大,而Token的目的就是为了减轻服务器的压力,减少频繁的查询数据库,是的服务器更加的见状。
- Token的定义: Token是服务端生成的一串字符串,以做客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需要带上Token前来请求数据即可,无需再次带上用户名和密码。(相当于 token 是 一种身份,不需要在此进行查询用户名和密码)
二、 JWT
- JWT 全称 json web token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。本质就是 一大串字符串。JWT本身没有安全可言。
三、 Redis
注:这里只对Redis 作简单介绍,说明 Token与Redis的关系。
-
redis定义:redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。
-
Redis 在Java Web 主要应用两个场景:
存储 缓存 用的数据;
需要高速读写的场合使用它快速读写;
-
Token 为什么存储到Redis?
-
Token 具有时效性,简单来说就是有效期,超过这个期限Token就会失效,需要用户重新登录。但是 怎么在项目中实现这个时效性,这个时候就用到Redis,Redis 天然支持设置过期时间以及
通过一些第三方提供包的API 到达自动续时的效果。
-
Redis 采用NoSQL 技术,可以支持每秒十几万次的读写操作,性能远高于数据库,在请求数量将多的时候,Redis也可以从容应对
-
用户登录信息一般不需要长效存储,放在内存中,可以减少数据库的压力
-
实现部分
引入依赖
<!--Jedis是Redis官方推荐的Java连接开发工具。-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- shiro 借用加密密码 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.4.RELEASE</version>
</dependency>
详细代码
1. 用户注册
@Override
@Transactional
public ReturnBody insertAdmin(SysAdmin admin) throws Exception {
//SysAdmin 为管理员类
admin.setAppName(Constants.PROJECT_NAME);//插入项目名
String password = admin.getUserPassword();
if(StringUtil.isEmpty(admin.getUserPassword())) {
password ="123456";//默认密码为123456
}
PasswordHash passwordHash = PasswordUtil.encrypt(password);//采用shrio加密方式(密码加盐)PasswordUtil继承DigestUtil(Apache DigestUtils线程安全的类)
admin.setUserPassword(passwordHash.getHexEncoded());//HexEncoded为加密过后的密文
SysUser user = new SysUser(admin);
ReturnBody result = userService.insertOrUpdateAuth(user);//判断用户邮箱手机号是否已经注册,用户名是否已经存在,这部分逻辑自己编写
ResultUtil.throwException(result);//如果请求失败,则抛出异常,
userMapper.insert(user);//插入用户表
admin.setUserId(user.getUserId());//mybatis-plus 自动生成主键,获得主键
this.save(admin);//插入管理员表
userSaltMapper.insert(new SysUserSalt(admin.getUserId(), passwordHash.getSalt()));//插入盐值与用户关联表(每个用户都有一个特定的盐值增强安全性)
Map<String, Object> map = new HashMap<String, Object>();
map.put("user_id", user.getUserId());
sysUserRoleMapper.deleteByMap(map);
List<String> roleIds = admin.getRoleId();
if (StringUtil.isNotEmpty(roleIds)){
int a = userRoleMapper.insertdeRoles(admin.getUserId(),roleIds);
}//插入用户角色关联信息
/*
* 根据admin的userId关联创建的附件列表
*/
ReturnBody body = new ReturnBody(admin);
((SysAdmin) body.getData()).setUserPassword(password);
return body;
}
PassHash 类 及 PasswordUtil 加密方法
PasswordUtil 加密方法
/**
* 盐值加密
* @param source
* @return
*/
public static PasswordHash encrypt(Object source) {
return new PasswordHash(source);
}
/**
* 密码加密配置
*/
public class PasswordHash {
/**
* 加密方式
*/
private String algorithmName;
/**
* 加密次数
*/
private int hashIterations;
/**
* 原密码
*/
private Object source;
/**
* 盐值
*/
private String salt;
/**
* 加密后的密文
*/
private String hexEncoded;
/**
*
* @param source
*/
public PasswordHash(Object source) {
this.algorithmName = "md5";//加密方式
this.hashIterations = 996;//加密次数
this.salt = UUIDUtil.getUUID();//盐值为随机数
this.source = source;//密码
this.hexEncoded = new SimpleHash(algorithmName, source, salt, hashIterations).toHex();//加密后的密文
}
@Override
public String toString() {
return "PasswordHash [algorithmName=" + algorithmName + ", hashIterations=" + hashIterations + ", source="
+ source + ", salt=" + salt + ", hexEncoded=" + hexEncoded + "]";
}
}
2. 用户登录
/**
* 方法作用: 获取令牌(统一用户登录入口)
* @param user
* @return
* @throws Exception
*/
@ApiOperation(value = "获取令牌(统一用户登录入口)" , notes="获取令牌(统一用户登录入口)")
@PostMapping(value="/login")
public String accessToken(@RequestBody Token token) throws Exception {
String password = null;
if (StringUtil.isNotEmpty(token.getPassword())){
password = RSACoder.decrypt(token.getPassword());//解码 BASE64解码
token.setPassword(password);
}
token = authService.selectByUserName(token);//通过用户名查到用户信息方便与传过来的token进行比对
if (null==token) {
ValueUtil.isError(ReturnCode.USERNAME_WETHER);
//用户名不存在,抛出异常自行封装
}else if(!PasswordUtil.checkPassword(password,token.getSalt(),token.getUserPassword())) {
//password为用户输入的密码字符串,salt为用户插入之初设置的盐值,token.getUserPassword为数据库中通过密码加盐的方式形成的密文
//帐号或密码错误,抛出异常自行封装
ValueUtil.isError(ReturnCode.LOGIN_FAILED);
}else if(StringUtil.isEmpty(token.getHomeUrl())) {
//判断用户是否设置主页,方便用户登录成功后跳转登录界面
ValueUtil.isError(ReturnCode.HOMEPAGE_NULL);
} else {
//设置用户权限(权限放面会陆续更新)
token.setResSet(authService.selectResourceByUserId(token.getUserId()));
//用户类型
token.setSelectTypes(authService.selectTypes(token.getUserId()));
token.setAppKey(appKey); //用户所属项目
token.setIp(getUserIp());//用户登录id地址
String tokenCode = tokenManager.putToken(token);//创建token
setResponseTokenCode(tokenCode);
token = tokenManager.queryToken(tokenCode);//查看token
token.setToken(tokenCode);
}
return ValueUtil.toJson(token);//返回token
}
这里封装了一个Token管理器便于管理token
Token管理器
public class TokenManager {
/**
*
*/
@Autowired
private RedisClientTokenUtil redisClient;//Redis操作Token封装类
/**
* ios端设备标识前缀
*/
private static final String APPKEY_PREFIX_IOS = "IOS";
/**
* android端设备标识前缀
*/
private static final String APPKEY_PREFIX_ANDROID = "ANDROID";
/**
* moblie端设备标识前缀
*/
private static final String APPKEY_PREFIX_MOBILE = "MOBILE";
/**
* web端设备标识前缀
*/
private static final String APPKEY_PREFIX_WEB = "WEB";
/**
* ios终端token有效期
*/
private static final int EXPIRE_SECOND_IOS = 14 * 24 * 60 * 60;
/**
* android终端token有效期
*/
private static final int EXPIRE_SECOND_ANDROID = 14 * 24 * 60 * 60;
/**
* MOBILE终端token有效期
*/
private static final int EXPIRE_SECOND_MOBILE = 14 * 24 * 60 * 60;
/**
* web终端token有效期
*/
private static final int EXPIRE_SECOND_WEB = 1 * 30 * 60;
/**
* 管理员终端token有效期
*/
private static final int ADMINISTRATOR_WEB = 3 * 60 * 60;
/**
* 其他终端token有效期
*/
private static final int EXPIRE_SECOND_OTHER = EXPIRE_SECOND_WEB;
/**
* 获取token
*
* @param token
* @return
* @throws Exception
*/
public String putToken(Token token) throws Exception {
String tokenCode = null;
if (token != null) {
token.setExpireSecond(createExpireSecond(token));//延长token 有效期
tokenCode = createTokenCode(token);//创建token
String key = getKey(tokenCode);//项目名+tokenCode
redisClient.set(key, token, token.getExpireSecond());
}
return tokenCode;
}
/**
* 校验token
*
* @param tokenCode
* @return
* @throws Exception
*/
public boolean checkToken(String tokenCode) throws Exception {
if (!StringUtils.isEmpty(tokenCode)) {
String key = getKey(tokenCode);
if (redisClient.exists(key)) {
Token token = redisClient.get(key);
// 重置有效期
redisClient.setExpire(key, token.getExpireSecond());
return true;
}
}
return false;
}
/**
* 查看token
*
* @param tokenCode
* @return Token
* @throws Exception
*/
public Token queryToken(String tokenCode) throws Exception {
String key = getKey(tokenCode);
Token token = redisClient.get(key);
if (token != null) {
redisClient.setExpire(key, token.getExpireSecond());
}
if (StringUtil.isEmpty(token)){
ValueUtil.isError(ReturnCode.UNAUTHORIZED);
}
return token;
}
/**
* 初始化token有效期
*
* @param token
* @return
*/
private int createExpireSecond(Token token) {
int expireSecond = EXPIRE_SECOND_OTHER;
if (token != null) {
String appKey = token.getAppKey();
if (!StringUtils.isEmpty(appKey)) {
String prefix = appKey;
switch (prefix) {
case APPKEY_PREFIX_ANDROID:
expireSecond = EXPIRE_SECOND_ANDROID;
break;
case APPKEY_PREFIX_IOS:
expireSecond = EXPIRE_SECOND_IOS;
break;
case APPKEY_PREFIX_MOBILE:
expireSecond = EXPIRE_SECOND_MOBILE;
break;
case APPKEY_PREFIX_WEB:
expireSecond = EXPIRE_SECOND_WEB;
}
} else {
if (token.getUserType() == 1) {
expireSecond = ADMINISTRATOR_WEB;
}
}
}
return expireSecond;
}
/**
* 根据tokenCode获取key
*
* @param tokenCode
* @return
*/
private String getKey(String tokenCode) {
return Constants.TOKEN_PREFIX + tokenCode;
}
/**
* 生成tokenCode
*
* @param token
* @return
*/
private String createTokenCode(Token token) {
String tokenCode = null;
if (StringUtil.isNotEmptyObjects(token.getUserId(),token.getUserName())) {
tokenCode=UserUtils.createToken(token.getUserId(),token.getUserName(),token.getExpireSecond());
}
return tokenCode;
}
//createToken 方法直接写在这里
public static String createToken(String userId, String userName, int expireSecond) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(RSAUtil.privateKey);//RSAUtil.privateKey为私钥
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder()
.claim("userId", userId) // 设置载荷信息
.claim("userName",userName)
.signWith(signatureAlgorithm, signingKey);
//生成JWT
return builder.compact();
}
}
密码校验
/**
* 密码校验
* @param source
* @param salt
* @param password
* @return
*/
public static boolean checkPassword(Object source, String salt, String password) {
//判断参数非空 ,自行封装 此处省略
return password.equals(encryptString(source, salt));//再次加密 比对加密结果
}
/**
* 盐值加密
* @param source
* @return
*/
public static String encryptString(Object source, String salt) {
return new PasswordHash(source, salt).getHexEncoded();
}
参考:链接:https://blog.csdn.net/weixin_44914766/article/details/105507646
2. 数据库的 索引 和 引擎
一、 一条查询语句是如何执行的
1.1 应用程序发现SQL 到服务端
当前执行SQL 语句时,应用程序会连接到相应的数据库服务器,然后 服务器对SQL进行处理
1.2 查询缓存
接着数据库服务器会先去查询是否有改SQL语句的缓存,key是查询的语句,value是查询的结果。
如果你查询能够直接命中,就会直接从缓存中拿出value来返回客户端。
注:查询不会被解析、不会生成执行计划、不会被执行
1.3 查询优化处理,生成执行计划
如果没有缓存命中,则开始执行第三步
- 解析SQL:生成解析树,验证关键字如select,where,left,join等是否正确
- 预处理:进一步检查解析树是否合法,如果检查数据表和列是否存在,验证用户权限等。
- 优化SQL:决定使用哪个索引,或者在多个表相关联的时候决定表的连接顺序,紧接着,将SQL语句转换成执行计划。
1.4 将查询结果返回客户端
最后,数据库服务器将查询结果返回给客户端,如果查询可以缓存,MySQL也会将结果放到查询缓存中。
二、 索引的概述
2.1 索引是什么
索引是帮助数据库高效获取数据的数据结构
2.2 索引的分类
从存储结构上来划分
- Btree索引 B+树 B-树
- 哈希索引
- full-index 全文索引
- RTree
从应用层次来划分
- 普通索引
- 唯一索引
- 复合索引
从表记录的排列顺序和索引的排列顺序是否一致来划分
- 聚集索引: 表记录的排列顺序和索引顺序一致。
- 非聚集索引:表记录的排列顺序和索引的排列顺序不一致。
2.3 聚集索引和非聚集索引
概念
- 聚集索引:就是主键创建的索引
- 非聚集索引:就是以非主键创建的索引 也叫做二级索引
详细概括
- 聚集索引
聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,因为只要找到第一个索引值记录,其余的连续性的记录在物理表中也会连续存放,一起就可以查到。
缺点:新增比较慢,因为为了保证表中记录的物理顺序和索引顺序一致,在记录插入的时候,会对数据页重新排序
- 非聚集索引
索引的逻辑顺序与磁盘上行的物理存储顺序不同,非聚集索引在叶子节点存储的是主键和索引列,当我们使用非聚集索引查询数据时,需要拿到叶子上的主键再去表中查到想要查找的数据。这个过程我们叫做回表。
聚集索引和非聚集索引的区别
- 聚集索引在叶子节点存储的是表中的数据
- 非聚集索引在叶子节点上存储的是主键和索引列
三、 索引底层数据结构
3.1 哈希索引
用哈希表来实现快速查找,就像我们平时用的hashmap一样,value=get(key) O(1)
定义
哈希索引就是采用一定的哈希算法,只需要一次哈希算法即可立刻定位到相应的位置,速度非常快。
本质上就是把键值换算称新的哈希值,根据这个哈希值来定位。
局限性
- 哈希索引没办法利用索引来完成排序
- 不能进行多字段查询
- 在有大量重复键值的情况下,哈希索引的效率极低 存在哈希碰撞问题。
- 不支持范围查询
在MySQL 常用的InnoDB 引擎中,还是使用B+树索引比较多,InnoDB是子实行哈希索引的哈希索引。
如何设计索引的数据结构
假设要查询某个区间的数据,我们只需要拿到区间的起始值,然后在树种进行查找。
B树
B树的特点:
- 关键字分布在整颗树的所有节点
- 任何一个关键字出现且只出现在一个节点中
- 搜索有可能在非叶子节点结束
- 其搜索性能等价于在关键字全集内做一次二分查找。
B+树
B+树基本特点
- 非叶子节点的子树指针与关键字个数相同
- 非叶子节点的子树指针P[i],只想关键字属于[k[i],k[i+1]]的子树
- 为所有叶子节点增加一个链指针
- 所有关键字都在叶子节点出现
B+树的特性
- 所有关键字都出现在叶子节点的链表中,且链表中的关键字是有序的。
- 搜索只在叶子节点命中
- 非叶子节点相当于是叶子节点的索引层,叶子节点是存储关键字数据的数据层。
相对于B树。B+树做索引的优势
- B+树的磁盘读写代价更低。内部没有指向关键字具体信息的指针,其内部节点相对于B树更小
- 树的查询效率更稳定。B+树所有数据都存在叶子节点,所有关键字查询的路径长度相同,每次数据的查询效率相当,而B+树可能在非叶子节点就停止查找了,所以查询不够稳定
- B+树只需要遍历叶子节点就可以实现整颗树的遍历
MongoDB 的索引为什么选择B树,而MySQL的索引是B+树?
因为MongoDB不是传统的关系型数据库,而是以Json格式作为存储的NoSQL的非关系型数据库,目得就是高性能、高可用、易拓展。
MyISAM存储引擎和InnoDB的索引有什么区别
-
MyISAM 存储引擎
主键索引
MyISAM 的索引文件和数据文件是分离的,索引文件仅保存记录所在页的指针,通过这些指针来读取页,进而存取被索引的行。
树种的叶子节点保存的是对应行的物理位置,通过该值,存储引擎能顺利的进行回表查询,得到一行完整记录。
同时,每个叶子也保存了指向下一个叶子的指针,从而方便叶子节点的范围扁你了
在MyISAM中,主键索引和辅助索引在结构上没有任何区别,只是主键索引要求key是唯一的,而辅助索引的key可以重复。
InnoDB存储引擎
InnoDB 的主键索引和辅助索引之提到过
-
主键索引及存储主键值,又存储行数据
-
辅助索引
对于是辅助索引,InnoDB采用的方式是在叶子节点中保存主键值,通过这个主键值来回表查询一条完整记录,因此按辅助索引检索其实进行了二次查询,效率是没有主键索引高的。
MySQL索引执行
当多条件联合查询时,优化器会评估 哪个条件的索引效率高,他会选择最佳索引去使用。也就是说,此处三个索引列都可能被用到,只不过优化器判断只需要使用userid这一个索引就能完成本次查询,故最终explain展示的key为userid
最后
索引优化策略
多个单列索引在多条件查询时优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引都用上。但是多个单列索引底层会建立多个B+索引树,比较占用空间,浪费搜索效率,所以多条件查询最好建立联合索引。
联合索引失效
-
违反最左匹配原则
-
在索引列上做任何操作 如(计算、函数、类型转换)
-
使用不等于
-
like中以通配符开头(’%abc’)
-
字符串不加单引号索引失效
-
连接索引失效
原文连接: https://blog.csdn.net/weixin_39622084/article/details/110433360
3. 一级缓存 和 二级缓存
一级缓存:mybatis 默认开启
二级缓存:在映射配置文件中开启
【一级缓存】
它指的是Mybatis中SqlSession对象的缓存
- 当执行查询之后,查询结果会同时存入到SqlSession为我们提供一块区域。该区域的结构是一个Map
- 当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否有,有的话直接拿出来用
- 当SqlSession对象消失时(发生了DML操作 插入 删除 修改),Mybatis的以及缓存也就消失。
【二级缓存】
它指的时Mybatis中SqlSessionFactory对象的缓存。
由同一个SqlSessionFactory 对象创建的SqlSession共享其内存
二级缓存的使用步骤:
- 让Mybatis框架支持二级缓存 在SqlMapConfig.xml中配置
- 让当前的映射文件支持二级缓存 在IUserDao.xml中配置
- 让当前的擦欧总支持二级缓存 在select标签中配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TYm420C-1634705895711)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20211012123444494.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChV1aQuA-1634705895750)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20211012123505062.png)]
本文链接:https://blog.csdn.net/weixin_43232955/article/details/105675200
4. 三次握手 二次不行 四次挥手 三次不行
-
第一次握手:客户端发送网络包,服务端收到了。
这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 -
第二次握手:服务端发包,客户端收到了。
这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。 -
第三次握手:客户端发包,服务端收到了。
这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。两次握手不可以
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
四次挥手
TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作
-
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。 -
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。 -
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。 -
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。挥手为什么需要四次?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
四次挥手释放连接时,等待2MSL的意义?
假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
原文链接:https://blog.csdn.net/hyg0811/article/details/102366854
-
5. HashMap 的 红黑树结构
红黑树,本质上来说是一颗二叉搜索树
即树中的任何节点的值大于它的左孩子,且小于它的右孩子。 任意节点的左、右子树也分别为二叉查找树
红黑树的主要特性:
(1)每个节点要么是黑色,要么是红色。(节点非黑即红)
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
6. spring Cloud 五大组件
1. Eurca
2. Feign Client
Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活
3. Ribbon
Feign怎么知道请求那台服务器呢,这是SpringCloud就派上用场了,Ribbon 务就是解决这个问题的,他的作用是负载均衡,会帮你在每一次请求的时候选择一台机器,均匀的把请求发送到各个机器上 ,Ribbon的负载均衡默认的使用RoundRobin轮询算法,什么是轮询算法?如果订单服务对库存发起十次请求,那就先让你请求第一台机器,然后是第二台机器,第三台机器,…接着循环到第十台机器
此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:
首先Ribbon会从 Eureka Client里面获取到对应的服务注册表,也就知道了所有的服务都部署在了那台机器上,在监听哪些端口,
然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器,
4. Hystrix
在微服务架构里,一个系统会有多个服务, 以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务,现在假设订单服务自己最多只有100个线程可以处理请求然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。
分析下这样会导致什么问题:如果系统在高并发的情况下,大量请求涌过来的时候,订单服务的100个线程会卡在积分服务这块,导致订单服务没有一个线程可以处理请求,然后会导致别的请求订单服务的时候,发现订单服务挂掉,不响应任何请求了,这种问题就是微服务架构中恐怖的服务器雪崩问题:如下图,这么多的服务互相调用要是不做任何保护的话,某一个服务挂掉会引起连锁反应,导致别的服务挂掉,比如积分服务挂了会导致订单服务的线程全部卡在请求积分服务这里没有一个线程可以工作,瞬间导致订单服务也挂了别人请求订单服务全部会卡住,无法响应。
5. zull
Zuul:微服务网关,这个组件是负责网络路由的!
什么事网络路由?
假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那就是中美合拍,文体开花了!
上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。
而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。
总结一下SpringCloud结果核心组件:
Eureka:个服务启动时,Eureka会将服务注册到EurekaService,并且EurakeClient还可以返回过来从EurekaService拉去注册表,从而知道服务在哪里
Ribbon:服务间发起请求的时候,基于Ribbon服务做到负载均衡,从一个服务的对台机器中选择一台
Feign:基于fegin的动态代理机制,根据注解和选择机器,拼接Url地址,发起请求
Hystrix:发起的请求是通过Hystrix的线程池来走,不同的服走不同的线程池,实现了不同的服务调度隔离,避免服务雪崩的问题
Zuul:如果前端后端移动端调用后台系统,统一走zull网关进入,有zull网关转发请求给对应的服务
原文链接:https://blog.csdn.net/plpldog/article/details/104961330
Zuul:微服务网关,这个组件是负责网络路由的!
什么事网络路由?
假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那就是中美合拍,文体开花了!
上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。
而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。
总结一下SpringCloud结果核心组件:
Eureka:个服务启动时,Eureka会将服务注册到EurekaService,并且EurakeClient还可以返回过来从EurekaService拉去注册表,从而知道服务在哪里
Ribbon:服务间发起请求的时候,基于Ribbon服务做到负载均衡,从一个服务的对台机器中选择一台
Feign:基于fegin的动态代理机制,根据注解和选择机器,拼接Url地址,发起请求
Hystrix:发起的请求是通过Hystrix的线程池来走,不同的服走不同的线程池,实现了不同的服务调度隔离,避免服务雪崩的问题
Zuul:如果前端后端移动端调用后台系统,统一走zull网关进入,有zull网关转发请求给对应的服务
原文链接:https://blog.csdn.net/plpldog/article/details/104961330