跳到主要内容

流量防卫兵-Sentinel

· 阅读需 9 分钟
季冠臣
后端研发工程师

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

官方文档:https://sentinelguard.io/zh-cn/index.html

image-20230205223154781

1、什么是Sentinel

  • 阿里巴巴开源的分布式系统流控工具
  • 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
  • 丰富的应用场景:消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
  • 完备的实时监控:Sentinel 同时提供实时的监控功能
  • 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合

在微服务中扮演者治理者的作用

image-20230205223327662

  • 核心概念:
    • 资源:是 Sentinel 中的核心概念之一,可以是java程序中任何内容,可以是服务或者方法甚至代码,总结起来就是我们要保护的东西
    • 规则:定义怎样的方式保护资源,主要包括流控规则、熔断降级规则等

核心概念

2、整合微服务项目实操

2.1、安装

  • Sentinel 分为两个部分

    • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo、Spring Cloud 等框架也有较好的支持。
    • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
  • 微服务引入Sentinel依赖

     <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
  • Sentinel控制台搭建

  • 文档:https://github.com/alibaba/Sentinel/wiki/控制台

  • 控制台包含如下功能:

    • 查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
    • 监控 (单机和集群聚合)通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。
    • 规则管理和推送:统一管理推送规则。
    • 鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。

    注意:Sentinel 控制台目前仅支持单机部署

//启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本,
//-Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080
//默认用户名和密码都是 sentinel

给sentinel-dashboard-1.8.2.jar增加权限
chomd 777 sentinel-dashboard-1.8.2.jar

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.2.jar

守护进程启动
nohub java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.2.jar

简化 默认端口8080
java -jar sentinel-dashboard-1.8.2.jar

界面效果

image-20230205231438988

2.2、限流

简单实操:

  • 多个微服务接入Sentinel配置

    spring:
    cloud:
    sentinel:
    transport:
    dashboard: 127.0.0.1:8080
    port: 9999

    #dashboard: 8080 控制台端口
    #port: 9999 本地启的端口,随机选个不能被占用的,与dashboard进行数据交互,会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描 默认为 8719
  • 微服务注册上去后,由于Sentinel是懒加载模式,所以需要访问微服务后才会在控制台出现

image-20230205233156696

  • 限流配置实操
    • 控制台配置

image-20230205233330855

2.3、流量控制(flow control)

  • 流控文档

  • 原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

  • 两种规则

    • 基于统计并发线程数的流量控制
    并发数控制用于保护业务线程池不被慢调用耗尽
    Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目)
    如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。
    流控规则会下发到微服务,微服务如果重启,则流控规则会消失可以持久化配置
    • 基于统计QPS的流量控制
    当 QPS 超过某个阈值的时候,则采取措施进行流量控制
  • 控制面板

    • 资源名:默认是请求路径,可自定义
    • 针对来源:对哪个微服务进行限流,默认是不区分来源,全部限流,这个是针对 区分上游服务进行限流, 比如 视频服务 被 订单服务、用户服务调用,就可以针对来源进行限流
  • 流控效果

image-20230205234311415

  • 流量控制的效果包括以下几种:

    • 直接拒绝:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝

    • Warm Up:冷启动/预热,如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值

    image-20200908212417778

    • 匀速排队:严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法,主要用于处理间隔性突发的流量,如消息队列,想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求

      image

      • 注意:
        • 匀速排队等待策略是 Leaky Bucket 算法结合虚拟队列等待机制实现的。
        • 匀速排队模式暂时不支持 QPS > 1000 的场景

2.4、熔断降级

  • 文档:https://github.com/alibaba/Sentinel/wiki/熔断降级

  • 熔断降级(虽然是两个概念,基本都是互相配合)

    • 对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一
    • 对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩
    • 熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置
  • 什么是Sentinel降级规则

  • Sentinel 熔断策略

    • 慢调用比例(响应时间): 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用

      • 比例阈值:修改后不生效-目前已经反馈给官方那边的bug
      • 熔断时长:超过时间后会尝试恢复
      • 最小请求数:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断

      image-20200909121342893

    • 异常比例:当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断

      • 比例阈值
      • 熔断时长:超过时间后会尝试恢复
      • 最小请求数:熔断触发的最小请求数,请求数小于该值时,即使异常比率超出阈值也不会熔断

      image-20200909121357918

    • 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断

      • 异常数:
      • 熔断时长:超过时间后会尝试恢复
      • 最小请求数:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断

      image-20200909121415806

服务熔断一般有三种状态(如图)

image-20230206000459097

  • 熔断关闭(Closed)

    • 服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
  • 熔断开启(Open)

    • 后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
  • 半熔断(Half-Open)

    • 所谓半熔断就是尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率
  • 熔断恢复:

    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态)尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。

    • 如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断状态

2.5、熔断框架比较

SentinelHystrixresilience4j
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口(LeapArray)滑动窗口(RxJava)Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性多个扩展点插件的形式接口的形式
基于注解支持支持支持支持
限流基于QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的Rate Limiter模式
系统自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统
浏览量:加载中...

smart-doc+Trona无侵入式API文档管理平台

· 阅读需 4 分钟
季冠臣
后端研发工程师

背景smart-doc (opens new window)+ Torna 组成行业领先的文档生成和管理解决方案,使用smart-doc无侵入完成Java源代码分析和提取注释生成API文档,自动将文档推送到Torna企业级接口文档管理平台。

我们先来看看效果图:

image-20230204155034231

image-20230204155007085

超级简洁 ,而且它是无侵入式的,不需要改动代码,只需要增加配置文件即可

官方仓库地址:https://gitee.com/durcframework/torna

为什么要用Torna?

接口文档解决方案,目标是让接口文档管理变得更加方便、快捷。Torna采用团队协作的方式管理和维护接口文档,将不同形式的文档纳入进来统一维护。

Torna弥补了传统文档生成工具(如swagger)的不如之处,在保持原有功能的前提下丰富并增强了一些实用的功能。

  • 不满足swagger文档预览页面和调试页面的展现方式
  • 不喜欢swagger这种侵入式注解
  • 希望使用javadoc注释生成文档,并进行接口调试
  • 希望把公司所有项目接口文档进行统一管理
  • 希望把项目中的若干文档提供给第三方调用者查看
  • 希望可以统一管理项目中的字典枚举

现在已经跟新到1.20.0 它也可以整合swagger使用,但是鉴于swagger侵入性高,官放推荐的是smart-doc+Torna这一技术路线。

如何使用?

我们以win为例进行部署。其他部署环境参照官网即可

1、下载zip本地运行

1.1、下载地址:https://gitee.com/durcframework/torna/releases

需要下载俩个jar包

image-20230204155607681

本地解压即可。

1.2、在资源包中找到mysql.sql脚本,导入mysql

1.3、在启动包中找到application.properties配置文件,修改数据库连接配置

image-20230204155910470

1.4、在启动目录用cmd运行startup.bat文件

image-20230204160003417

1.5、本地浏览器访问http://localhost:7700

1.6、登录文档管理平台 初始账户 ---> 用户名:admin,密码:123456

1.7、后续升级,只需要覆盖torna.jar文件dist文件夹,然后重启即可

2、Torna整合smart-doc教程

通过这套组合您可以实现:只需要写完Java注释就能把接口信息推送到Torna平台,从而实现接口预览、接口调试。

官方地址:https://torna.cn/dev/smart-doc.html

2.1、只需规范编写代码注释即可

image-20230204160646490

2.2、Torna服务配置

全图形化操作 可以根据需求创建空间管理员和使用空间

image-20230204160817710

创建自己的项目文档名 ,创建好后如左侧样例。

image-20230204160930669

查看token是否生成,一会要在项目里配置

image-20230204161207568

2.3、cloud服务配置

我的demo项目结构是这样的

image-20230204161338397

在跟目录的pom中添加smart-doc插件

		<!-- smart-doc插件 -->
<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.4.9</version>
<configuration>
<!--指定生成文档的使用的配置文件-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>视频项目</projectName>
</configuration>
<executions>
<execution>
<phase>package</phase>
</execution>
</executions>
</plugin>

在web中的resources下添加一个smart-doc.json文件

这里需要注意要在根目录下创建resources包,并标记为资源包

image-20230204162619302

我们以video-service为例 其他模块一样

{
"outPath": "target/doc",
"projectName": "video-cloud项目",
"packageFilters": "space.jachen.controller.*",
"openUrl": "http://localhost:7700/api",
"appToken": "8de92ae464124cff815ce75e7732b9ca",
"debugEnvName":"本地环境",
"debugEnvUrl":"http://127.0.0.1:8080",
"tornaDebug": true,
"replace": true
}

参数说明:

  • outPath:固定填这个不用变
  • projectName:项目名称
  • packageFilters:Controller接口对应的package目录,多个用;隔开
  • openUrl:Torna中的OpenAPI接口
  • appToken:Torna中的OpenAPI token
  • debugEnvName:Torna中调试环境名称
  • debugEnvUrl:Torna中调试环境地址
  • tornaDebug:是否开启调试,初次使用建议开始,后面稳定了关闭
  • replace:是否替换文档,建议true

over,已经配置完成,接下来最后一步,推送到torna

2.4、推送文档到Torna

在项目根目录输入maven命令:

mvn smart-doc:torna-rest -pl :jachen-cloud -am

其中-pl :shop-web -am表示推送哪个子模块

看到最后的

image-20230204162947190

表示推送成功,即可前往Torna接口列表查看文档了

浏览量:加载中...

负载均衡之Ribbon实现

· 阅读需 6 分钟
季冠臣
后端研发工程师

背景Ribbon是一个用于客户端负载均衡的开源项目,它基于Netflix Hystrix库,用于提供客户端侧负载均衡算法。它可以在微服务架构中帮助您控制客户端对服务器的访问,以实现负载均衡、断路器和智能路由。Ribbon提供了一组API,可以在您的应用程序代码中集成负载均衡功能,从而提高系统的可用性和稳定性。

这个是开源地址:https://github.com/Netflix/ribbon

在接受这个厉害的组件前先 讲讲什么负载均衡和常见的解决方案

  • 什么是负载均衡(Load Balance)

    分布式系统中一个非常重要的概念,当访问的服务具有多个实例时,需要根据某种“均衡”的策略决定请求发往哪个节点,这就是所谓的负载均衡,
    原理是将数据流量分摊到多个服务器执行,减轻每台服务器的压力,从而提高了数据的吞吐量
  • 软硬件角度负载均衡的种类

    • 通过硬件来进行解决,常见的硬件有NetScaler、F5、Radware和Array等商用的负载均衡器,但比较昂贵的
    • 通过软件来进行解决,常见的软件有LVS、Nginx等,它们是基于Linux系统并且开源的负载均衡策略
  • 从端的角度负载均衡有两种

    • 服务端负载均衡
    • 客户端负载均衡

    image-20200908114732828

  • 常见的负载均衡策略(看组件的支持情况)

    • 节点轮询
      • 简介:每个请求按顺序分配到不同的后端服务器
    • weight 权重配置
      • 简介:weight和访问比率成正比,数字越大,分配得到的流量越高
    • 固定分发
      • 简介:根据请求按访问ip的hash结果分配,这样每个用户就可以固定访问一个后端服务器
    • 随机选择、最短响应时间等等

AlibabaCloud集成Ribbon实现负载均衡

我们来看看它在哪个包下?这个是Nacos整合ribbon的包

image-20230202235459188

  • 什么是Ribbon

    Ribbon是一个客户端负载均衡工具,通过Spring Cloud封装,可以轻松和AlibabaCloud整合

  • 订单服务增加@LoadBalanced 注解

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

接着我的上一篇文章,https://blog.jiguanchen.space/blog/product-nacos

  • 调用实战
Video v = restTemplate.getForObject("http://jachen-video-service"
+ "/api/v1/video/getById/" + id, Video.class);

// 注意:方便看到负载均衡效果,在video类增加这个字段,记录当前机器ip+端口

它的实操非常简单,那它底层的源码是怎么走的呢?我们断点看一下。

Ribbon负载均衡源码走读

  • 分析思路
    • 通过直接找入口

image-20230203000427363

  • 分析@LoadBalanced
    • 1)首先从注册中心获取provider的列表
    • 2)通过一定的策略选择其中一个节点
    • 3)再返回给restTemplate调用

image-20230203000859449

image-20230203001105830

image-20230203001158208

image-20230203001503881

最后返回调用的服务

image-20230203001601629

Ribbon支持的负载均衡策略

策略类命名描述
RandomRule随机策略随机选择server
RoundRobinRule轮询策略按照顺序选择server(默认)
RetryRule重试策略当选择server不成功,短期内尝试选择一个可用的server
AvailabilityFilteringRule可用过滤策略过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server(active connections超过配置的阈值)
WeightedResponseTimeRule响应时间加权重策略根据server的响应时间分配权重,以响应时间作为权重,响应时间越短的服务器被选中的概率越大,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间
ZoneAvoidanceRule区域权重策略综合判断server所在区域的性能,和server的可用性,轮询选择server

怎么配置呢?

订单服务增加配置
jachen-video-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  • 策略选择: 1、如果每个机器配置一样,则建议不修改策略 (推荐) 2、如果部分机器配置强,则可以改为 WeightedResponseTimeRule

上面就是ribbon的底层实现,它还是美中不足的。存在的问题:不规范,风格不统一,维护性比较差

那么为什么存在这些问题呢?

我们来看上面的代码

image-20230204225604362

每次我们都要去在代码里这样拼路径,如果我的方法特别多呢?很显然这种方法是不可取的,所以也就引出了Feign组件

什么是Feign

SpringCloud提供的伪http客户端(本质还是用http),封装了Http调用流程,更适合面向接口化
让用Java接口注解的方式调用Http请求.

不用像Ribbon中通过封装HTTP请求报文的方式调用 Feign默认集成了Ribbon

官方文档:https://spring.io/projects/spring-cloud-openfeign

  • Nacos支持Feign,可以直接集成实现负载均衡的效果 可以使用springboot等注解

改造微服务 集成Open-Feign

  • Feign让方法调用更加解耦

  • 使用feign步骤讲解

    • 加入依赖(order客户端)

              <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
    • 配置注解(order客户端)

    //启动类增加
    @EnableFeignClients
    • 增加一个远程调用接口(可以在order模块里面也可以单独拿出去自己一个模块)
    //订单服务增加接口,服务名称记得和nacos保持一样
    @FeignClient(name="jachen-video-service")
    • 编写代码
    @GetMapping(value = "/api/v1/video/find_by_id")
    Video findById(@PathVariable int videoId);

    // 一定注意上面错误的 一定要注意路径一定完整
    // @RequestParam里面都要指定
    // 如果参数要用restful风格接收 用@PathVariable
    // 路径的参数如果和方法里参数不一样 一定要加("id")参数
    @GetMapping("/api/v1/video/getById/{id}")
    Video getById(@RequestParam("id") Integer id);
    • 注入videoservice(在order客户端controller层加入)就像在本地使用一样简单
        @Autowired
    private VideoService videoService;

    ...

    Video v = videoService.getById(id);

    ...

    Ribbon和feign两个的区别和选择

    选择feign
    默认集成了ribbon
    写起来更加思路清晰和方便
    采用注解方式进行配置,配置熔断等方式方便
浏览量:加载中...

服务治理之Nacos

· 阅读需 8 分钟
季冠臣
后端研发工程师

背景Nacos官网这样介绍自己:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。那么它到底是什么?它是怎么使用的呢?它的出现帮助我们解决了哪些问题?这是本文探究的重点。

Nacos官方文档:https://nacos.io/zh-cn/docs/what-is-nacos.html。

Nacos到底是什么?

Nacos就是注册中心+配置中心的组合(Nacos = Eureka+Config+Bus)

Nacos 的关键特性包括:

  • 服务发现和服务健康监测

    Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDKOpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODOHTTP&API查找和发现服务。

    Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

  • 动态配置服务

    动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

    动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

    配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

    Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

  • 动态 DNS 服务

    动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。

    Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

  • 服务及其元数据管理

    Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

据说nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验.....

举个简单的例子 现在有一个Order订单的服务要去远程调用Video视频的服务完成下单操作。

OrderController类:

/**
* @author JaChen
* @date 2023/1/31 17:26
*/
@RestController
@RequestMapping("api/v1/order")
public class OrderController {

@Autowired
RestTemplate restTemplate;

/**
* 下单的方法
* @param id
* @return
*/
@GetMapping("/save/{id}")
public Object save(@PathVariable Integer id){
Video v = restTemplate
// 这里路径是写死的 如果ip变了 我们每次都要更改源码
.getForObject("http://127.0.0.1:9000/api/v1/video/getById/"
+ id, Video.class);
if (v != null) {
VideoOrder.builder()
.videoId(v.getId())
.videoTitle(v.getTitle())
.createTime(new Date());
}else {
return ResponseUtil.resultMap(false,444,"下单失败");
}
return ResponseUtil.resultMap(true,200,"下单成功"
,VideoOrder.VideoOrderBuilder.class);
}
}

VideoController类

**
* @author JaChen
* @date 2023/1/31 16:21
*/
@RestController
@RequestMapping("api/v1/video")
public class VideoController {

@Autowired
VideoService videoService;

@GetMapping("getById/{id}")
public Object getById(@PathVariable Integer id){

Video video = videoService.getById(id);

return ResponseUtil.resultMap(true,200,"查询成功",video);
}
}

当Order服务去调用Video服务的时候,.getForObject("http://127.0.0.1:9000/api/v1/video/getById/"+id, Video.class);这里路径是写死的 ,如果服务的ip换了或者新增其他服务 ,需要我们去更改源码 ,得不偿失,那么有没有一个应用,可以在Order服务完成下单操作时就知道哪些服务是“活着”的呢?Nacos一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台随时应运而生,所有服务启动前都会把自己注册到Nacos注册中心进行统一管理,同时,比如订单服务想去远程调用我们的其他服务 可以去注册中心查找这些服务的状态信息,实现动态去获取他们的地址,这样就避免了自身源码的改动,也为实现负载均衡等其他功能提供了基础。

总结

什么是注册中心(服务治理)

  • 服务注册:服务提供者provider,启动的时候向注册中心上报自己的网络信息
    • 服务发现:服务消费者consumer,启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息
  • 核心:服务管理,是有个服务注册表,心跳机制动态维护,服务实例在启动时注册到服务注册表,并在关闭时注销。

nacosMap

  • 特性大图:要从功能特性,非功能特性,全面介绍我们要解的问题域的特性诉求
  • 架构大图:通过清晰架构,让您快速进入 Nacos 世界
  • 业务大图:利用当前特性可以支持的业务场景,及其最佳实践
  • 生态大图:系统梳理 Nacos 和主流技术生态的关系
  • 优势大图:展示 Nacos 核心竞争力
  • 战略大图:要从战略到战术层面讲 Nacos 的宏观优势

为什么要用

  • 微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,维护带来很大问题

Nacos是怎么使用的呢?

1、安装

1.1 Linux/Mac安装Nacos

  • 解压安装包
  • 进入bin目录
  • 启动 sh startup.sh -m standalone
  • 访问 localhost:8848/nacos/index.html
  • 默认账号密码 nacos/nacos

1.2 Win安装

  • 解压安装包
  • 进入bin目录
  • 启动 startup.cmd -m standalone
  • 访问 localhost:8848/nacos/index.html
  • 默认账号密码 nacos/nacos

默认:MODE="cluster"集群方式启动,如果单机启动需要设置-m tandalone参数,否则,启动失败。

2、登录页面

http://localhost:8848/nacos/index.html#/login

image-20230131223518460

果然是国内的技术 页面看着就是亲切~

官网文档:https://spring.io/projects/spring-cloud-alibaba#learn

3、配置

3.1在Order服务和Video服务都 添加相关依赖

<!--添加nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

3.2 在yml文件配置server-addr

  cloud:
nacos:
server-addr: 127.0.0.1:8848

3.3 在启动类上开启服务注册

@EnableDiscoveryClient

现在我们已经把这俩个服务注册到了Nacos上

image-20230131230001317

3.4 引入DiscoveryClient用于查找服务的客户端并修改OrderController.java

// 获取服务列表 (可能是集群)
List<ServiceInstance> instanceList = discoveryClient
.getInstances("jachen-video-service");
ServiceInstance instance = instanceList.get(0);
Video v = restTemplate.getForObject(instance.getUri()
+ "/api/v1/video/getById/" + id, Video.class);

3.5 Nacos怎么开启负载均衡呢?

    /**
* 远程调用
* @return RestTemplate对象
*/
@Bean
// 在RestTemplate请求的时候开启负载均衡。
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}

一个注解就搞定了。使用Ribbon可以这样直接指定服务名

        // 获取服务列表 (可能是集群)
List<ServiceInstance> instanceList = discoveryClient
.getInstances("jachen-video-service");
Video v = restTemplate.getForObject("http://jachen-video-service"
+ "/api/v1/video/getById/" + id, Video.class);

为什么可以实现负载均衡呢?

image-20230131233001382

总结:

  1. Nacos是一个开源的分布式服务发现、配置管理和服务管理平台。
  2. 功能全面:Nacos不仅支持服务注册与发现,还支持配置管理和服务管理。
  3. 易于使用:Nacos的接口简洁明了,容易上手。
  4. 性能优秀:Nacos采用了高性能的分布式架构,能够承受大量的服务注册与查询。
  5. 活跃的社区:Nacos有活跃的社区支持,问题能够得到快速解决。
浏览量:加载中...

Redis缓存常见异常及解决方案

· 阅读需 10 分钟
季冠臣
后端研发工程师

背景Redis是一种流行的内存数据结构存储,广泛用作数据库、缓存和消息代理。Redis 主要用来做缓存使用,在提高数据查询效率、保护数据库等方面起到了关键性的作用,很大程度上提高系统的性能。当然在使用过程中,也会出现一些异常情景,导致 Redis 失去缓存作用。

Redis常见应用

  1. 缓存:Redis通常用作缓存,以存储常用数据,以减少数据库查询次数。
  2. 会话管理:Redis用于存储Web应用程序中的用户会话数据。
  3. 实时分析:Redis用于处理和分析大量的实时数据。
  4. 队列管理:Redis用于实现消息队列,以异步分发和处理任务。
  5. 排行榜和计数:Redis用于维护游戏、社交媒体和其他应用中项目的实时排名和计数。
  6. 发布/订阅消息:Redis支持发布/订阅消息模式,并用于实时聊天和通知系统。

Redis常见缓存异常

缓存雪崩 缓存穿透 缓存击穿

1、缓存雪崩

1.1现象

  • 发生在缓存数据在同一时刻大量失效,比如缓存数据的过期时间设置为同一时刻。
  • 由于缓存中的数据失效,大量的请求直接请求后端数据,导致系统请求量瞬间增加,造成系统压力过大,从而宕机。

image-20230130104242186

1.2原因

  • 缓存服务不可用。
  • 缓存服务可用,但是大量 KEY 同时失效。

1.3解决方案

  1. 缓存服务不可用 redis 的部署方式主要有单机、主从、哨兵和 cluster 模式。
  • 单机 只有一台机器,所有数据都存在这台机器上,当机器出现异常时,redis 将失效,可能会导致 redis 缓存雪崩。
  • 主从 主从其实就是一台机器做主,一个或多个机器做从,从节点从主节点复制数据,可以实现读写分离,主节点做写,从节点做读。 优点:当某个从节点异常时,不影响使用。 缺点:当主节点异常时,服务将不可用。
  • 哨兵 哨兵模式也是一种主从,只不过增加了哨兵的功能,用于监控主节点的状态,当主节点宕机之后会进行投票在从节点中重新选出主节点。 优点:高可用,当主节点异常时,自动在从节点当中选择一个主节点。 缺点:只有一个主节点,当数据比较多时,主节点压力会很大。
  • cluster 模式 集群采用了多主多从,按照一定的规则进行分片,将数据分别存储,一定程度上解决了哨兵模式下单机存储有限的问题。 优点:高可用,配置了多主多从,可以使数据分区,去中心化,减小了单台机子的负担. 缺点:机器资源使用比较多,配置复杂。
  • 小结 从高可用得角度考虑,使用哨兵模式和 cluster 模式可以防止因为 redis 不可用导致的缓存雪崩问题。
  1. 大量 KEY 同时失效 可以通过设置永不失效、设置不同失效时间、使用二级缓存和定时更新缓存失效时间
  • 设置永不失效 如果所有的 key 都设置不失效,不就不会出现因为 KEY 失效导致的缓存雪崩问题了。redis 设置 key 永远有效的命令如下: PERSIST key 缺点:会导致 redis 的空间资源需求变大。
  • 设置随机失效时间 如果 key 的失效时间不相同,就不会在同一时刻失效,这样就不会出现大量访问数据库的情况。 redis 设置 key 有效时间命令如下: Expire key 示例代码如下,通过 RedisClient 实现
/**
* 随机设置小于30分钟的失效时间
* @param redisKey
* @param value
*/
private void setRandomTimeForReidsKey(String redisKey,String value){
//随机函数
Random rand = new Random();
//随机获取30分钟内(30*60)的随机数
int times = rand.nextInt(1800);
//设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(redisKey,value,times);
}
  • 使用二级缓存 二级缓存是使用两组缓存,1 级缓存和 2 级缓存,同一个 Key 在两组缓存里都保存,但是他们的失效时间不同,这样 1 级缓存没有查到数据时,可以在二级缓存里查询,不会直接访问数据库。 示例代码如下:
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从1级缓存中获取数据
String value = test.queryByOneCacheKey("key");
//如果1级缓存中没有数据,再二级缓存中查找
if(StringUtils.isBlank(value)){
value = test.queryBySecondCacheKey("key");
//如果二级缓存中没有,从数据库中查找
if(StringUtils.isBlank(value)){
value =test.getFromDb();
//如果数据库中也没有,就返回空
if(StringUtils.isBlank(value)){
System.out.println("数据不存在!");
}else{
//二级缓存中保存数据
test.secondCacheSave("key",value);
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("数据库中返回数据!");
}
}else{
//一级缓存中保存数据
test.oneCacheSave("key",value);
System.out.println("二级缓存中返回数据!");
}
}else {
System.out.println("一级缓存中返回数据!");
}
}
  • 异步更新缓存时间

    每次访问缓存时,启动一个线程或者建立一个异步任务来,更新缓存时间。

    示例代码如下:

public class CacheRunnable implements Runnable {

private ClusterRedisClientAdapter redisClient;
/**
* 要更新的key
*/
public String key;

public CacheRunnable(String key){
this.key =key;
}

@Override
public void run() {
//更细缓存时间
redisClient.expire(this.getKey(),1800);
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}
}
public static void main(String[] args) {
CacheTest test = new CacheTest();
//从缓存中获取数据
String value = test.getFromCache("key");
if(StringUtils.isBlank(value)){
//从数据库中获取数据
value = test.getFromDb("key");
//将数据放在缓存中
test.oneCacheSave("key",value);
//返回数据
System.out.println("返回数据");
}else{
//异步任务更新缓存
CacheRunnable runnable = new CacheRunnable("key");
runnable.run();
//返回数据
System.out.println("返回数据");
}
}

2、缓存穿透

2.1现象

  • 发生在请求的数据不存在于缓存,但是请求依然会到达后端服务器。
  • 由于请求的数据不存在,每次请求都会到达后端服务器,导致大量的无效请求,增加了后端的压力。

image-20230130110129433

2.2异常原因

  • 非法调用

2.3解决方案

  1. 对于不存在的数据设置一个空值,设置有效期
  2. 对请求数据做限流
  3. 哈希/布隆过滤器预处理
  4. 使用布尔值进行标识(例如:0或1)表示数据是否存在
  5. 双重检查机制(Cache Aside Pattern)

TODO 这里只写了第一种解决方案,剩下的以后补充...

  • 缓存空值 当缓存和数据库中都没有值时,可以在缓存中存放一个空值,这样就可以减少重复查询空值引起的系统压力增大,从而优化了缓存穿透问题。 示例代码如下:
private String queryMessager(String key){
//从缓存中获取数据
String message = getFromCache(key);
//如果缓存中没有 从数据库中查找
if(StringUtils.isBlank(message)){
message = getFromDb(key);
//如果数据库中也没有数据 就设置短时间的缓存
if(StringUtils.isBlank(message)){
//设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(key,null,60);
}else{
redisClient.setNxEx(key,message,1800);
}
}
return message;
}

缺点:大量的空缓存导致资源的浪费,也有可能导致缓存和数据库中的数据不一致。

3、缓存击穿

3.1现象

当一个高流量的请求访问同一个缓存数据时,如果该数据的缓存失效,那么所有的请求都会直接请求数据库,造成数据库压力过大。

与缓存穿透区别:缓存击穿是因为某个数据被大量请求,导致其缓存失效,从而打击数据库;缓存穿透是因为请求了不存在的数据,导致所有请求都需要访问数据库。

image-20230130111239309

3.2异常原因

  • 热点 KEY 失效的同时,大量相同 KEY 请求同时访问。

3.3解决方案

  1. 加锁机制:对于特定的数据请求加锁,避免大量请求造成的缓存失效。
  2. 分布式锁:使用分布式锁来确保对特定数据的更新操作是原子性的。
  3. 后备机制:在缓存失效时使用后备机制,保证缓存请求能够得到响应。
  4. 限流机制:对请求数据进行限流,防止大量请求造成的压力。
  5. 对于过期的数据进行异步删除:避免因数据过期导致的缓存击穿。
  • 分布式锁

    使用分布式锁,同一时间只有 1 个请求可以访问到数据库,其他请求等待一段时间后,重复调用。

    示例代码如下

/**
* 根据key获取数据
* @param key
* @return
* @throws InterruptedException
*/
public String queryForMessage(String key) throws InterruptedException {
//初始化返回结果
String result = StringUtils.EMPTY;
//从缓存中获取数据
result = queryByOneCacheKey(key);
//如果缓存中有数据,直接返回
if(StringUtils.isNotBlank(result)){
return result;
}else{
//获取分布式锁
if(lockByBusiness(key)){
//从数据库中获取数据
result = getFromDb(key);
//如果数据库中有数据,就加在缓存中
if(StringUtils.isNotBlank(result)){
oneCacheSave(key,result);
}
}else {
//如果没有获取到分布式锁,睡眠一下,再接着查询数据
Thread.sleep(500);
return queryForMessage(key);
}
}
return result;
}

还可以预先设置热门数据,通过一些监控方法,及时收集热点数据,将数据预先保存在缓存中。

总结:

  • 缓存雪崩是因为缓存数据的失效导致的请求增加,造成系统压力过大,宕机。
  • 缓存穿透是因为请求的数据不存在,导致请求直接到达后端,增加了后端的压力。
  • 缓存击穿是因为某个数据被大量请求,导致其缓存失效,从而打击数据库。

其他常见的Redis错误及其解决方法:

  1. 连接被拒绝:当Redis没有运行或防火墙阻止了连接时会发生此错误。解决方案:检查Redis是否正在运行,如果防火墙阻止了连接,请为Redis添加一个例外。
  2. 达到最大客户端数:当太多客户端试图同时连接到Redis时会发生此错误。解决方案:增加Redis配置文件中的maxclients设置,或添加更多的Redis实例。
  3. 内存不足:当Redis的内存用尽时会发生此错误。解决方案:减小存储在Redis中的数据的大小,为服务器添加更多内存,或使用Redis的逐出策略自动删除最近最少使用的项目。
  4. 语法错误:当命令以错误的语法发送到Redis时会发生此错误。解决方案:检查命令的语法,然后重试。
  5. 未找到键:当尝试检索不存在的键的值时会发生此错误。解决方案:使用EXISTS命令在尝试检索其值之前检查键是否存在。
  6. 将数据加载到内存:当Redis从磁盘重新加载数据到内存时会发生此错误。解决方案:等待重新加载完成,或增加Redis配置文件中的保存间隔。
浏览量:加载中...

jedis实操redisAPI

· 阅读需 11 分钟
季冠臣
后端研发工程师

为了加深对原生redis命令的认识,我又手写了边通过jedis调用原生redis API 的实操,Redis命令十分丰富,包括的命令组有Cluster、Connection、Geo、Hashes、HyperLogLog、Keys、Lists、Pub/Sub、Scripting、Server、Sets、Sorted Sets、Strings、Transactions一共14个redis命令组两百多个redis命令,Redis中文命令大全。您可以通过下面的检索功能快速查找命令,已下是全部已知的redis命令列表。如果您有兴趣的话也可以查看我们的网站结构图,它以节点图的形式展示了所有redis命令。

Redis官网:https://redis.io/

一、使用Java操作Redis方式

1、Jedis

Jedis 是直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的,需要使用连接池

其API提供了比较全面的Redis命令的支持,相比于其他Redis 封装框架更加原生

Jedis中的方法调用是比较底层的暴露的Redis的API,Java方法基本和Redis的API保持着一致

使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作

2、Lettuce

高级Redis客户端,用于线程安全同步,异步响应

基于Netty的的事件驱动,可以在多个线程间并发访问, 通过异步的方式可以更好的利用系统资源

我就用Jedis操作了.....

二、亲笔演练Redis五大常用命令组

1、使用连接池

package space.jachen.jedis;

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

/**
*
* TODO: 测试jedis与redis建立连接 使用线程池
*
* @author JaChen
* @date 2022/12/10 15:37
*/
public class Test2 {


public static void main(String[] args) {

// 建立连接池配置信息
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(10*100);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);

// 创建一个连接池
JedisPool pool = new JedisPool(poolConfig, "192.168.253.128", 6379);

// 从连接池获取一个现成的连接
Jedis jedis = pool.getResource();
System.out.println("jedis = " + jedis);

// 不使用的时候放回连接池
jedis.close();


}


}

2、操作String

package space.jachen.jedis;

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

/**
*
* @author JaChen
* @date 2022/12/10 15:41
*/
public class TestString {


public static void main(String[] args) {

// 1、建立连接池配置信息
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(10*100);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);

// 2、创建一个连接池
JedisPool pool = new JedisPool(poolConfig, "192.168.253.128", 6379);

// 3、从连接池获取一个现成的连接
Jedis jedis = pool.getResource();
System.out.println("jedis = " + jedis);

// 4、操作String
System.out.println("===============String==============");
// TODO:存储字符串的类型,key-value
// notice:
// 1、值的长度不能超过512 MB
// 2、key命名规范,不要过长,冒号分割,业务名:表名:ID

// TODO: 应用 :
// 1、验证码
// 2、计时器、发号器
// 3、重复订单提交令牌
// 4、热点商品卡片(序列化对象存储)
// 5、分布式锁
String setName = jedis.set("name", "jachen");
System.out.println("setName = " + setName);
System.out.println(jedis.get("name"));

jedis.set("age","18");
System.out.println(jedis.get("age"));

// incr 对应 的值 进行加1操作,并返回新值。
jedis.incr("age");
System.out.println(jedis.get("age"));

// 将key对应的数字加increment。(如果key不存在,操作之前,key会被置为零。)
jedis.incrBy("age",10);
System.out.println(jedis.get("age"));

jedis.set("cart","887766");
System.out.println(jedis.get("cart"));

// 批量设置或获取多个key的值
jedis.mset("tell", "110", "weight", "65kg");
System.out.println(jedis.mget("tell","weight"));

jedis.setnx("sex","0");
System.out.println(jedis.get("sex"));

// 原子性操作
// 将key设置值为value,如果key不存在等同SET命令。
jedis.msetnx("k1","v1","k2","v2","sex","0");
System.out.println(jedis.mget("k1","k2","sex"));

// 当key存在时什么也不做, 是set if not exists的简写。
jedis.msetnx("k1","v1","k2","v2","sex","1");
System.out.println(jedis.mget("k1","k2","sex"));

// 设置key对应字符串value,
// 并且设置key在给定的seconds时间之后超时过期,原子操作
jedis.setex("car",2,"887766");
System.out.println(jedis.get("car"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(jedis.get("car"));

// 5、不使用的时候放回连接池
jedis.close();

}
}

运行结果:

image-20221210231902515

3、操作List

package space.jachen.jedis;

import redis.clients.jedis.Jedis;

import java.util.List;

/**
* @author JaChen
* @date 2022/12/10 21:05
*/
public class TestList {

public static void main(String[] args) {


// 1、获取一个Jedis连接
Jedis jedis = new Jedis("192.168.253.128",6379);

// 2、操作List
// TODO: 字符串列表,按照插入顺序排序
// 存储结构:双向链表,插入删除时间复杂度O(1)快,查找为O(n)慢
// notice:
// 1、通常添加一个元素到列表的头部(左边)或者尾部(右边)
// 2、存储的都是string字符串类型
// 3、支持分页操作,高并发项目中,第一页数据都是来源list,第二页和更多信息则是通过数据库加载
// 4、一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表不超过40亿个元素)

// TODO: 应用:
// 1、简单队列
// 2、最新评论列表
// 3、非实时排行榜:定时计算榜单,如手机日销榜单
System.out.println("===============List===============");

// ① 将一个或多个值插入到列表头部 key value1 [value2]
jedis.lpush("phone","mate5","iphone14","iphone11","iphone5s");
System.out.println(jedis.llen("phone"));
System.out.println(jedis.lrange("phone",0,-1));

// ② 移除并获取列表最后一个元素
String rpop = jedis.rpop("phone");
System.out.println("rpop = " + rpop);

// ③ 在key对应的list的尾部添加一个元素
jedis.rpush("netbook","DELL","Mac","外星人");
System.out.println(jedis.lrange("netbook",0,-1));

// ④ 从key对应的list的尾部删除一个元素,并返回该元素
System.out.println(jedis.lpop("netbook"));

// ⑤ 移出并获取列表的最后一个元素,
// 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
// BRPOP LIST1 LIST2 .. LISTN TIMEOUT
jedis.del("netbook","iphone");
System.out.println(jedis.lrange("iphone",0,-1));
jedis.rpush("iphone","a","b","c");
List<String> list = jedis.brpop("iphone", "netbook", "1");
for (String s : list) {
System.out.println(s);
}

// ⑥ 移除元素,可以指定移除个数
jedis.lrem("iphone",1,"a");
System.out.println(jedis.lrange("iphone",0,-1));

// 3、关闭连接
jedis.close();

}

}

运行结果:

image-20221210232207004

4、操作Set

package space.jachen.jedis;

import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Set;

/**
* @author JaChen
* @date 2022/12/10 21:40
*/
public class TestSet {


public static void main(String[] args) {

// 1、获取一个Jedis连接
Jedis jedis = new Jedis("192.168.253.128",6379);

// TODO: 将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略
// notice: 集合是通过哈希表实现的

// TODO: 应用:
// 1、去重
// 2、社交应用关注、粉丝、共同好友
// 3、统计网站的PV、UV、IP
// 4、大数据里面的用户画像标签集合
// 2、操作Set
System.out.println("===============Set==============");

// ① 添加一个或多个指定的member元素到集合的 key中.
// 指定的一个或者多个元素member 如果已经在集合key中存在则忽略
jedis.sadd("user1","u1","beijing","18","iphone11");
jedis.sadd("user2","u2","shanghai","19","iphone12");
jedis.sadd("user3","u3","beijing","19","iphone13");

// ② 返回集合存储的key的基数 (集合元素的数量).
System.out.println(jedis.scard("user1"));
System.out.println(jedis.scard("user2"));
System.out.println(jedis.scard("user3"));

// ③ 返回的集合元素是第一个key的集合与后面所有key的集合的差集
System.out.println(jedis.sdiff("user1", "user2", "user3"));

// ④ 返回指定所有的集合的成员的交集.
System.out.println(jedis.sinter("user1", "user2"));

// ⑤ 返回成员 member 是否是存储的集合 key的成员.
System.out.println(jedis.sismember("user1","beijing"));

// ⑥ 在key集合中移除指定的元素. 如果指定的元素不是key集合中的元素则忽略
jedis.srem("user1","iphone11");
System.out.println(jedis.scard("user1"));

// ⑦ 返回给定的多个集合的并集中的所有成员.
Set<String> sunion = jedis.sunion("user1", "user2", "user3");
for (String s : sunion) {
System.out.print(s + " ");
}
System.out.println();

// ⑧ 返回key集合所有的元素.
Set<String> user1 = jedis.smembers("user1");
for (String s : user1) {
System.out.print(s + " ");
}

// key的存活时间
System.out.println("\njedis.ttl(\"user1\") = " + jedis.ttl("user1"));

// 3、关闭连接
jedis.close();

}
}

运行结果:

image-20221210232225594

5、操作Hash

package space.jachen.jedis;

import redis.clients.jedis.Jedis;

import java.util.HashMap;

/**
* @author JaChen
* @date 2022/12/10 22:03
*/
public class TestHash {

public static void main(String[] args) {


// 1、获取一个Jedis连接
Jedis jedis = new Jedis("192.168.253.128",6379);


// 2、Hash操作
// TODO:string类型的field和value的映射表,hash特别适合用于存储对象
// notice:每个 hash 可以存储 232 - 1 键值对(40多亿)

// TODO:应用:
// 1、购物车
// 2、用户个人信息
// 3、商品详情
System.out.println("===============Hash==============");

// ① 设置 key 指定的哈希集中指定字段的值
jedis.hset("class","redis","80");
System.out.println(jedis.hget("class","redis"));

HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("java","100");
hashMap.put("c++","90");
hashMap.put("c","96");
jedis.hmset("class",hashMap);
// ② 返回 key 指定的哈希集中所有的字段和值
System.out.println(jedis.hgetAll("class"));

// ③ 从 key 指定的哈希集中移除指定的域
jedis.hdel("class","c","c++");
System.out.println(jedis.hgetAll("class"));
// ④ 返回hash里面field是否存在
System.out.println(jedis.hexists("class","c"));

// ⑤ 增加 key 指定的哈希集中指定字段的数值, 如果是-1 则是递减
// HINCRBY key field increment
jedis.hincrBy("class","java",-5);
System.out.println(jedis.hgetAll("class"));
jedis.hincrBy("class","java",-5);
System.out.println(jedis.hgetAll("class"));

// 3、关闭Jedis连接
jedis.close();

}

}

运行结果:

image-20221210232256172

6、操作SortedSet

package space.jachen.jedis;

import redis.clients.jedis.Jedis;

import java.util.HashMap;

/**
* @author JaChen
* @date 2022/12/10 22:36
*/
public class TestSortedSet {

public static void main(String[] args) {

// 1、获取一个Jedis连接
Jedis jedis = new Jedis("192.168.253.128",6379);


// 2、操作SortedSet 可叫zSet
// TODO:
// 1、有序集合可以看做是在Set集合的的基础上为集合中的每个元素维护了一个顺序值: score,
// 它允许集合中的元素可以按照score进行排序
// 2、如果某个成员已经是有序集的成员,
// 那么更新这个成员的分数值,分数值可以是整数值或双精度浮点数。
// 3、用于将一个或多个成员元素及其分数值加入到有序集当中
// notice:
// 1、底层使用到了Ziplist压缩列表和“跳跃表”两种存储结构
// 2、如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果

// TODO:
// 应用:
// 1、实时排行榜:商品热销榜、体育类应用热门球队、积分榜
// 2、优先级任务、队列
// 3、朋友圈 文章点赞-取消,逻辑:用户只能点赞或取消,
// 统计一篇文章被点赞了多少次,可以直接取里面有多少个成员
System.out.println("===============SortedSet==============");

// ① 向有序集合添加一个或多个成员,或者更新已存在成员的分数
jedis.zadd("goods",100,"iPhone14");

// ② 获取有序集合的成员数
System.out.println(jedis.zcard("goods"));

// ③ 有序集合中对指定成员的分数加上增量 increment
jedis.zincrby("goods",10,"iPhone14");
System.out.println(jedis.zscore("goods","iPhone14"));
jedis.zincrby("goods",10,"iPhone14");
System.out.println(jedis.zscore("goods","iPhone14"));

HashMap<String, Double> hashMap = new HashMap<>();
hashMap.put("meta8",60D);
hashMap.put("iPhone12",101D);
hashMap.put("iPhone6s",30D);
jedis.zadd("goods",hashMap);
// ④ 计算在有序集合中指定区间分数的成员数
System.out.println(jedis.zcount("goods",10,150));
// ⑤ 通过索引区间返回有序集合指定区间内的成员, 成员的位置按分数值递增(从小到大)来排序
System.out.println(jedis.zrange("goods",0,-1));
// 根据score正序输出
System.out.println(jedis.zrangeByScore("goods",0D,100));
// ⑥ 通过索引区间返回有序集合指定区间内的成员, 成员的位置按分数值递增(从大到小)来排序
System.out.println(jedis.zrevrange("goods",0,-1));
// ⑦ 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
System.out.println(jedis.zrevrank("goods","meta8"));
// ⑧ 返回有序集key中成员member的排名。其中有序集成员按score值递增(从小到大)顺序排列
System.out.println(jedis.zrank("goods","meta8"));


// 3、 关闭jedis连接
jedis.close();


}
}

运行结果:

image-20221210232340442

总结

除了要了解这些数据结构的常用场景之前,还有继续加深对他们底层实现的了解,比如SortedSet底层为什么要使用跳跃表呢?为什么使用红黑树等等。

浏览量:加载中...

SpringMVC架构实现流程

· 阅读需 5 分钟
季冠臣
后端研发工程师

**起因:**SpringMVC是 Spring 框架提供的一款基于 MVC 模式的轻量级 Web 开发框架,为了让自己更清晰的认识SpringMVC底层的实现流程,对SpringMVC有更加深层次的认识,对它进行了使用流程的使用与总结

一、什么是SpringMVC

Spring MVC 使用 MVC 架构模式的思想,将 Web 应用进行职责解构,把一个复杂的 Web 应用划分成模型(Model)、控制器(Contorller)以及视图(View)三层。

  • Model:负责对请求进行处理,并将结果返回给 Controller;
  • View:负责将请求的处理结果进行渲染,展示在客户端浏览器上;
  • Controller:是 Model 和 View 交互的纽带;主要负责接收用户请求,并调用 Model 对请求处理,然后将 Model 的处理结果传递给 View。

Spring MVC 本质是对 Servlet 的进一步封装,其最核心的组件是 DispatcherServlet,它是 Spring MVC 的前端控制器,主要负责对请求和响应的统一地处理和分发。Controller 接收到的请求其实就是 DispatcherServlet 根据一定的规则分发给它的。

二、SpringMVC常用组件

Spring MVC 核心组件简要说明

Spring MVC 核心组件说明

1. 前端控制器(DispatcherServlet)

DispatcherServlet 是 Spring MVC 的核心组件,本质上是一个 Servlet,负责统一接收并分发所有的请求。它作为整个流程的控制中心,协调各个组件完成任务,从而降低了组件之间的耦合性,增强了系统的可扩展性。

  • 提供者:框架自带

2. 处理器映射器(HandlerMapping)

HandlerMapping 的作用是根据请求的 URL 或 HTTP 方法查找对应的处理器(Handler),也就是具体的控制器方法。

  • 提供者:框架自带

3. 处理器(Handler)

Handler 是具体的控制器方法(Controller),负责在 DispatcherServlet 的调度下处理用户的具体请求,并返回响应数据。

  • 提供者:自己编写

4. 处理器适配器(HandlerAdapter)

HandlerAdapter 负责调用与 HandlerMapping 映射到的处理器方法。它通过特定的规则执行处理器(Handler),确保方法能够被正确调用。

  • 提供者:框架自带

5. 视图解析器(ViewResolver)

ViewResolver 的功能是将逻辑视图名解析为物理视图路径。例如,将逻辑视图名 result 解析为 /WEB-INF/templates/result.html,并将解析后的视图返回给 DispatcherServlet。

  • 提供者:框架自带

6. 视图(View)

View 是最终的展示层,用于将模型数据(Model)通过页面呈现给用户。通常是由 JSP、Thymeleaf 或其他模板引擎生成的页面。

  • 提供者:自己编写

  1. DispatcherServlet(前端控制器)、HandlerMapping(处理器映射器)、HandlerAdapter(处理器适配器)、Handler(处理器)、ViewResolver(视图解析器)和 View(视图)。

三、SpringMVC基本流程图

image-20221204230911409

1、浏览器发送请求——>DispatcherServlet

该请求会被 DispatcherServlet(前端控制器)拦截;前端控制器收到请求后自己不进行处理,而是委托给其他组件进行处理,作为统一访问点,进行全局的流程控制。

<!-- DS:前端控制器 -->
<servlet>
<servlet-name>ds</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-web.xml</param-value>
</init-param>

<load-on-startup>1</load-on-startup>
</servlet>

2、DispatcherServlet——>HandlerMapping

DispatcherServlet 调用 HandlerMapping(处理器映射器)找到具体的处理器(Handler)及拦截器,最后以 HandlerExecutionChain 执行链的形式返回给 DispatcherServlet。

3、DispatcherServlet——>HandlerAdapter

DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器)。处理器适配器将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器。

4、HandlerAdapter——>Handler

根据 Handler 信息找到并执行相应的 Handler(即 Controller 控制器)对请求进行处理,Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC 的底层对象,包括 Model 数据模型和 View 视图信息)。

5、DispatcherServlet——>ViewResolver

DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析。

6、DispatcherServlet——>View

ViewResolver 解析完成后,会将 View 视图并返回给 DispatcherServlet。DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图)。

7、DispatcherServlet——>响应

返回控制权给DispatcherServlet,由它响应用户,到此一个流程结束。

四、SpringMVC的优势

Spring MVC 框架内部采用松耦合、可插拔的组件结构,具有高度可配置性,比起其他的 MVC 框架更具有扩展性和灵活性。此外,Spring MVC 的注解驱动(annotation-driven)和对 REST 风格的支持,也是它最具有特色的功能。

Spring MVC 是 Spring 框架的众多子项目之一,自 Spring 框架诞生之日起就包含在 Spring 框架中了,它可以与 Spring 框架无缝集成,在性能方面具有先天的优越性。对于开发者来说,Spring MVC 的开发效率要明显高于其它的 Web 框架,因此 Spring MVC 在企业中得到了广泛的应用,成为目前业界最主流的 MVC 框架之一。

Spring MVC 基于原生的 Servlet 实现,通过功能强大的前端控制器 DispatcherServlet,对请求和响应进行统一处理。

浏览量:加载中...

减少切换上下文开销

· 阅读需 2 分钟
季冠臣
后端研发工程师

我们怎么能减少切换上下文带来的开销呢?

cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。

上下文的频繁切换,会带来一定的性能开销

那么如何减少上下文切换的开销

1、无锁并发编程 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程2处理不同段的数据

2、CAS Java的Atomic包使用CAS算法来更新数据,而不需要加锁。使用最少线程

3、使用最少线程 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态

4、协程 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

浏览量:加载中...

RDB和AOF持久化小结

· 阅读需 4 分钟
季冠臣
后端研发工程师

背景:简单总结了一些Redis的两种持久化方式 RDB和AOF

Redis RDB持久化流程

RDB其实就是把数据以快照的形式保存在磁盘上,也是默认的持久化方式。这种方式是将内存中的数据以快照的方式写入到二进制文件中,默认文件名dump.rdb

RDB 的触发条件有手动触发和自动触发两种:

手动触发: Save命令: 会同步阻塞redis服务器进程 直到rdb文件创建完毕为止 在服务器阻塞期间不能处理命令请求(基本已经弃用了) Bgsave命令: 会在主线程fork一个子进程 由子进程去创建一份rdb文件 临时存储数据 父进程继续执行其他指令。

自动触发: 1、Save m n: 在配置文件中设置 save m n 指定当m秒内发生n次变化时 会发生bgsave。 2、其他自动触发机制 在主从复制场景下 从节点执行全量操作 则主节点会执行bgsave 执行shutdown命令 会自动执行rdb持久化。

优势:

  • RDB文件紧凑 全量备份 非常时候适合用于备份和灾难恢复

  • 生产RDB文件的时候 redis主进程会fork()一个子进程处理所有工作,主进程不需要进行任何磁盘IO操作

  • RDB在恢复大数据集时的速度比ADF的恢复速度快很多

劣势:

  • 当进行快照持久化时,会开启一个子进程专门负责快照持久化 子进程会拥有父进程的内存数据 父进程修改内存 子进程无影响 所以在快照持久化期间修改的数据不会被保存 可能丢失数据。

Redis AOF持久化流程

AOF的工作机制很简单,redis会将每个收到的写的命令通过write函数追加到文件中 也就是我们所说的日志记录

持久化原理:

每当有一个写命令过来 就直接保存在我们的AOF文件中 文件重写原理: AOF持久化的文件会越来越大 ,为了压缩AOF文件的持久化文件。Redis提供了bgrewriteaof命令 将内存中的数据以命令的方式保存在临时文件中,同时会frok出一条新进程来将文件重写。重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存的数据库内容用命令的方式重写了个新的AOF文件,于快照类似。

三种触发机制:

每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差 但数据完整性比较好。 每秒同步everysec:异步操作 每秒记录 如果一秒内宕dang机,有数据丢失。 no:操作系统控制的写回 性能好 宕机时丢失数据较多

优势:

  • AOF可以更好的保护数据不丢失 一般会每隔1秒 通过一个后台线程执行一次fsync操作 最多丢失1秒数据

  • AOF日志文件即使过大的时候 出现后台重写操作 也不会影响客户端的读写

  • 三种写回策略体现了一个重要的原则 trade-off 取舍 ,指在性能和可靠性保证之间做出取舍 即用空间换时间 体现出了redis的高性能。

劣势:

  • 对于同一份数据来说 AOF日志文件通常比RDB数据快照文件更大

  • AOF开启后 支持QPS会比EDB支持的写QPS低 因为AOF一般会配置fsync一次日志文件 每秒一次fsync 性能也很高

  • 以前AOF发生过bug 就是AOF记录的日志 进行数据恢复的时候 没有恢复一模一样的数据处理。


总结

  1. Redis 默认开启RDB持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。
  2. RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。
  3. Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。
  4. AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。
  5. Redis 针对 AOF文件大的问题,提供重写的瘦身机制。
  6. 若只打算用Redis 做缓存,可以关闭持久化。
  7. 若打算使用Redis 的持久化。建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。
浏览量:加载中...

浅谈事务隔离级别

· 阅读需 4 分钟
季冠臣
后端研发工程师

背景:今天在复习事务的隔离级别的时候总结了一些新的想法,之前只知道事务的隔离级别分为读未提交、读已提交、可重复读、序列化四个隔离级别,而在并发运行中,出现的隔离级别中的脏读、幻读和不可重复读的区别,以及他们的触发条件不是很清,经过了一番折腾,这里总结了一些想法。

1、那么究竟什么是脏读、幻读和不可重复读呢?

  • 脏读:一个事务读取了另一个事务未提交数据;
  • 不可重复读:同一个事务中前后两次读取同一条记录不一样。因为被其他事务修改了并且提交了。
  • 幻读:一个事务读取了另一个事务新增、删除的记录情况,记录数不一样,像是出现幻觉。

2、什么是事务的隔离级别呢?如何查看隔离级别?(dos 命令)

事务隔离是数据库处理的基础之一。隔离是首字母缩略词 ACID中的 I ;隔离级别是在多个事务同时进行更改和执行查询时微调性能与结果的可靠性、一致性和可再现性之间的平衡的设置。

/*
mysql支持四个隔离级别:
read-uncommitted:会出现脏读、不可重复读、幻读
read-committed:可以避免脏读,会出现不可重复读、幻读
repeatable-read:可以避免脏读、不可重复读、幻读。但是两个事务不能操作(写update,delete)同一个行。
serializable:可以避免脏读、不可重复读、幻读。但是两个事务不能操作(写update,delete)同一个表。

修改隔离级别:
set transaction_isolation='隔离级别';
#mysql8之前 transaction_isolation变量名是 tx_isolation

查看隔离级别:
select @@transaction_isolation; 默认为:repeatable-read可重复读
*/

数据库提供的 4 种事务隔离级别:

隔离级别描述
read-uncommitted允许A事务读取其他事务未提交和已提交的数据。会出现脏读、不可重复读、幻读问题
read-committed只允许A事务读取其他事务已提交的数据。可以避免脏读,但仍然会出现不可重复读、幻读问题
repeatable-read确保事务可以多次从一个字段中读取相同的值。在这个事务持续期间,禁止其他事务对这个字段进行更新。可以避免脏读和不可重复读。但是幻读问题仍然存在。注意:mysql中使用了MVCC多版本控制技术,在这个级别也可以避免幻读。
serializable确保事务可以从一个表中读取相同的行,相同的记录。在这个事务持续期间,禁止其他事务对该表执行插入、更新、删除操作。所有并发问题都可以避免,但性能十分低下。

image-20211202002655521

image-20211202002704464

image-20211202002714841

image-20211202002723256

总结

  1. 脏读:是在读未提交时发生的,事务A读取了事务B还未提交的数据,是一种错误。
  2. 幻读:发生在读未提交、读已提交和可重复读级别都会出现,它强调的是记录数不一样,即增删的情况。事务A读取了事务B已提交的数据,发生了幻读,它不是错误。
  3. 不可重复读:发生在读未提交和读已提交的隔离级别,它强调的是数据内容发生改变,即修改的情况。事务A读取了事务B已提交的数据,事务A前后两次读取同一条记录不一样,它也不是错误。
浏览量:加载中...