SpringCloud Gateway保姆级入门教程

news2024/11/25 0:22:36

什么是微服务网关

SpringCloud Gateway是Spring全家桶中一个比较新的项目,Spring社区是这么介绍它的:

该项目借助Spring WebFlux的能力,打造了一个API网关。旨在提供一种简单而有效的方法来作为API服务的路由,并为它们提供各种增强功能,例如:安全性,监控和可伸缩性。

而在真实的业务领域,我们经常用SpringCloud Gateway来做微服务网关,如果你不理解微服务网关和传统网关的区别,可以阅读此篇文章 Service Mesh和API Gateway关系深度探讨 来了解两者的定位区别。

以我粗浅的理解,传统的API网关,往往是独立于各个后端服务,请求先打到独立的网关层,再打到服务集群。而微服务网关,将流量从南北走向改为东西走向(见下图),微服务网关和后端服务是在同一个容器中的,所以也有个别名,叫做Gateway Sidecar。

image

为啥叫Sidecar,这个词应该怎么理解呢,吃鸡里的三蹦子见过没:

image

摩托车是你的后端服务,而旁边挂着的额外座椅就是微服务网关,他是依附于后端服务的(一般是指两个进程在同一个容器中),是不是生动形象了一些。

由于本人才疏学浅,对于微服务相关概念理解上难免会有偏差。就不在此详细讲述原理性的文字了。

本文只探讨SpringCloud Gateway的入门搭建和实战踩坑。 如果小伙伴们对原理感兴趣,可以等后续原理分析文章。

注:本文网关项目在笔者公司已经上线运行,每天承担百万级别的请求,是经过实战验证的项目。

文章目录

  • 手把手造一个网关
    • 引入pom依赖
    • 编写yml文件
    • 接口转义问题
    • 获取请求体(Request Body)
  • 踩坑实战
    • 获取客户端真实IP
    • 尾缀匹配
  • 总结

源代码

完整项目源代码已经收录到我的Github:

https://github.com/qqxx6661/springcloud_gateway_demo

手把手造一个网关

引入pom依赖

我使用了spring-boot 2.2.5.RELEASE作为parent依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

在dependencyManagement中,我们需要指定sringcloud的版本,以便保证我们能够引入我们想要的SpringCloud Gateway版本,所以需要用到dependencyManagement:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

最后,是在dependency中引入spring-cloud-starter-gateway:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

如此一来,我们便引入了2.2.5.RELEASE版本的网关:

image

此外,请检查一下你的依赖中是否含有spring-boot-starter-web,如果有,请干掉它。因为我们的SpringCloud Gateway是一个netty+webflux实现的web服务器,和Springboot Web本身就是冲突的。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

做到这里,实际上你的项目就已经可以启动了,运行SpringcloudGatewayApplication,得到结果如图:

image

编写yml文件

SpringBoot的核心概念是约定优先于配置,在以前初学Spring时,一直不理解这句话的意思,在使用SpringCloud Gateway时,更加深入的理解了这句话。在默认情况下,你不需要任何的配置,就能够运行起来最基本的网关。针对你之后特定的需求,再去追加配置。

而SpringCloud Gateway更强大的一点就是内置了非常多的默认功能实现,你需要的大部分功能,比如在请求中添加一个header,添加一个参数,都只需要在yml中引入相应的内置过滤器即可。

可以说,yml是整个SpringCloud Gateway的灵魂。

一个网关最基本的功能,就是配置路由,在这方面,SpringCloud Gateway支持非常多方式。比如:

  • 通过时间匹配
  • 通过 Cookie 匹配
  • 通过 Header 属性匹配
  • 通过 Host 匹配
  • 通过请求方式匹配
  • 通过请求路径匹配
  • 通过请求参数匹配
  • 通过请求 ip 地址进行匹配

这些在官网教程中,都有详细的介绍,就算你百度下,也会有很多民间翻译的入门教程,我就不再赘述了,我只用一个请求路径做一个简单的例子。

在公司的项目中,由于有新老两套后台服务,我们使用不同的uri路径进行区分。

  • 老服务路径为:url/api/xxxxxx,服务端口号为8001
  • 新服务路径为:url/api/v2/xxxxx,服务端口号为8002

那么可以直接在yml里面配置:

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=gateway-env, springcloud-gateway
      routes:
        - id: "server_v2"
          uri: "http://127.0.0.1:8002"
          predicates:
            - Path=/api/v2/**
        - id: "server_v1"
          uri: "http://127.0.0.1:8001"
          predicates:
            - Path=/api/**

上面的代码解释如下:

  • logging:由于文章需要,我们打开gateway和netty的Debug模式,可以看清楚请求进来后执行的流程,方便后续说明。
  • default-filters:我们可以方便的使用default-filters,在请求中加入一个自定义的header,我们加入一个KV为gateway-env:springcloud-gateway,来注明我们这个请求经过了此网关。这样做的好处是后续服务端也能够看到。
  • routes:路由是网关的重点,相信读者们看代码也能理解,我配置了两个路由,一个是server_v1的老服务,一个是server_v2的新服务。请注意,一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发。由于我们老服务的路由是/xx,所以需要将老服务放在后面,优先匹配词缀/v2的新服务,不满足的再匹配到/xx。

来看一下http://localhost:8080/api/xxxxx的结果:

image

来看一下http://localhost:8080/api/v2/xxxxx的结果:

image

可以看到两个请求被正确的路由了。由于我们真正并没有开启后端服务,所以最后一句error请忽略。

接口转义问题

在公司实际的项目中,我在搭建好网关后,遇到了一个接口转义问题,相信很多读者可能也会碰到,所以在这里我们最好是防患于未然,优先处理下。

问题是这样的,很多老项目在url上并没有进行转义,导致会出现如下接口请求,http://xxxxxxxxx/api/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

这样请求过来,网关会报错:

java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

在不修改服务代码逻辑的前提下,网关其实已经可以解决这件事情,解决办法就是升级到2.1.1.RELEASE以上的版本。

The issue was fixed in version spring-cloud-gateway 2.1.1.RELEASE.

所以我们一开始就是用了高版本2.2.5.RELEASE,避免了这个问题,如果小伙伴发现之前使用的版本低于 2.1.1.RELEASE,请升级。

获取请求体(Request Body)

在网关的使用中,有时候会需要拿到请求body里面的数据,比如验证签名,body可能需要参与签名校验。

但是SpringCloud Gateway由于底层采用了webflux,其请求是流式响应的,即 Reactor 编程,要读取 Request Body 中的请求参数就没那么容易了。

网上谷歌了很久,很多解决方案要么是彻底过时,要么是版本不兼容,好在最后参考了这篇文章,终于有了思路:

https://www.jianshu.com/p/db3b15aec646

首先我们需要将body从请求中拿出来,由于是流式处理,Request的Body是只能读取一次的,如果直接通过在Filter中读取,会导致后面的服务无法读取数据。

SpringCloud Gateway 内部提供了一个断言工厂类ReadBodyPredicateFactory,这个类实现了读取Request的Body内容并放入缓存,我们可以通过从缓存中获取body内容来实现我们的目的。

首先新建一个CustomReadBodyRoutePredicateFactory类,这里只贴出关键代码,完整代码请看可运行的Github仓库:

@Component
public class CustomReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomReadBodyRoutePredicateFactory.Config> {

    protected static final Log log = LogFactory.getLog(CustomReadBodyRoutePredicateFactory.class);
    private List<HttpMessageReader<?>> messageReaders;

    @Value("${spring.codec.max-in-memory-size}")
    private DataSize maxInMemory;

    public CustomReadBodyRoutePredicateFactory() {
        super(Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    public CustomReadBodyRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
        super(Config.class);
        this.messageReaders = messageReaders;
    }

    @PostConstruct
    private void overrideMsgReaders() {
        this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
    }

    @Override
    public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
        return new AsyncPredicate<ServerWebExchange>() {
            @Override
            public Publisher<Boolean> apply(ServerWebExchange exchange) {
                Class inClass = config.getInClass();
                Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
                if (cachedBody != null) {
                    try {
                        boolean test = config.predicate.test(cachedBody);
                        exchange.getAttributes().put("read_body_predicate_test_attribute", test);
                        return Mono.just(test);
                    } catch (ClassCastException var6) {
                        if (CustomReadBodyRoutePredicateFactory.log.isDebugEnabled()) {
                            CustomReadBodyRoutePredicateFactory.log.debug("Predicate test failed because class in predicate does not match the cached body object", var6);
                        }
                        return Mono.just(false);
                    }
                } else {
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
                        return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), CustomReadBodyRoutePredicateFactory.this.messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
                            exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
                        }).map((objectValue) -> {
                            return config.getPredicate().test(objectValue);
                        }).thenReturn(true);
                    });
                }
            }

            @Override
            public String toString() {
                return String.format("ReadBody: %s", config.getInClass());
            }
        };
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async.");
    }
}

代码主要作用:在有body的请求到来时,将body读取出来放到内存缓存中。若没有body,则不作任何操作。

这样我们便可以在拦截器里使用exchange.getAttribute("cachedRequestBodyObject")得到body体。

对了,我们还没有演示一个filter是如何写的,在这里就先写一个完整的demofilter。

让我们新建类DemoGatewayFilterFactory:

@Component
public class DemoGatewayFilterFactory extends AbstractGatewayFilterFactory<DemoGatewayFilterFactory.Config> {

    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    public DemoGatewayFilterFactory() {
        super(Config.class);
        log.info("Loaded GatewayFilterFactory [DemoFilter]");
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("enabled");
    }

    @Override
    public GatewayFilter apply(DemoGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }
            log.info("-----DemoGatewayFilterFactory start-----");
            ServerHttpRequest request = exchange.getRequest();
            log.info("RemoteAddress: [{}]", request.getRemoteAddress());
            log.info("Path: [{}]", request.getURI().getPath());
            log.info("Method: [{}]", request.getMethod());
            log.info("Body: [{}]", (String) exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY));
            log.info("-----DemoGatewayFilterFactory end-----");
            return chain.filter(exchange);
        };
    }

    public static class Config {

        private boolean enabled;

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

这个filter里,我们拿到了新鲜的请求,并且打印出了他的path,method,body等。

我们发送一个post请求,body就写一个“我是body”,运行网关,得到结果:

image

是不是非常清晰明了!

你以为这就结束了吗?这里有两个非常大的坑。

1. body为空时处理

上面贴出的CustomReadBodyRoutePredicateFactory类其实已经是我修复过的代码,里面有一行.thenReturn(true)是需要加上的。这才能保证当body为空时,不会报出异常。至于为啥一开始写的有问题,显然因为我偷懒了,直接copy网上的代码了,哈哈哈哈哈。

2. body大小超过了buffer的最大限制

这个情况是在公司项目上线后才发现的,我们的请求里body有时候会比较大,但是网关会有默认大小限制。所以上线后发现了频繁的报错:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

image

谷歌后,找到了解决方案,需要在配置中增加了如下配置

spring: 
  codec:
    max-in-memory-size: 5MB

把buffer大小改到了5M。

你以为这就又双叕结束了,太天真了,你会发现可能没有生效。

问题的根源在这里:我们在spring配置了上面的参数,但是我们自定义的拦截器是会初始化ServerRequest,这个DefaultServerRequest中的HttpMessageReader会使用默认的262144

所以我们在此处需要从Spring中取出CodecConfigurer, 并将里面的Reader传给serverRequest。

详细的debug过程可以看这篇参考文献:

http://theclouds.io/tag/spring-gateway/

OK,找到问题后,就可以修改我们的代码,在CustomReadBodyRoutePredicateFactory里,增加:

@Value("${spring.codec.max-in-memory-size}")
private DataSize maxInMemory;

@PostConstruct
private void overrideMsgReaders() {
  this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
}

这样每次就会使用我们的5MB来作为最大缓存限制了。

依然提醒一下,完整的代码可以请看可运行的Github仓库

讲到这里,入门实战就差不多了,你的网关已经可以上线使用了,你要做的就是加上你需要的业务功能,比如日志,延签,统计等。

踩坑实战

获取客户端真实IP

很多时候,我们的后端服务会去通过host拿到用户的真实IP,但是通过外层反向代理nginx的转发,很可能就需要从header里拿X-Forward-XXX类似这样的参数,才能拿到真实IP。

在我们加入了微服务网关后,这个复杂的链路中又增加了一环。

这不,如果你不做任何设置,由于你的网关和后端服务在同一个容器中,你的后端服务很有可能就会拿到localhost:8080(你的网关端口)这样的IP。

这时候,你需要在yml里配置PreserveHostHeader,这是SpringCloud Gateway自带的实现:

filters:
  - PreserveHostHeader # 防止host被修改为localhost

字面意思,就是将Host的Header保留起来,透传给后端服务。

filter里面的源码贴出来给大家:

public GatewayFilter apply(Object config) {
    return new GatewayFilter() {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            exchange.getAttributes().put(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, true);
            return chain.filter(exchange);
        }

        public String toString() {
            return GatewayToStringStyler.filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this).toString();
        }
    };
}

尾缀匹配

公司的项目中,老的后端仓库api都以.json结尾(/api/xxxxxx.json),这就催生了一个需求,当我们对老接口进行了重构后,希望其打到我们的新服务,我们就要将.json这个尾缀切除。可以在filters里设置:

filters:
  - RewritePath=(?<segment>/?.*).json, $\{segment} # 重构接口抹去.json尾缀

这样就可以实现打到后端的接口去除了.json后缀。

总结

本文带领读者一步步完成了一个微服务网关的搭建,并且将许多可能隐藏的坑进行了解决。最后的成品项目在笔者公司已经上线运行,并且增加了签名验证,日志记录等业务,每天承担百万级别的请求,是经过实战验证过的项目。

最后再发一次项目源码仓库:

https://github.com/qqxx6661/springcloud_gateway_demo

感谢大家的支持,如果文章对你起到了一丁点帮助,请点赞转发支持一下!

你们的反馈是我持续更新的动力,谢谢~

参考

https://cloud.tencent.com/developer/article/1449300

https://juejin.cn/post/6844903795973947400#heading-3

https://segmentfault.com/a/1190000016227780

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/multi/multi__reactor_netty_access_logs.html

https://www.cnblogs.com/savorboard/p/api-gateway.html

https://www.servicemesher.com/blog/service-mesh-and-api-gateway/

https://www.cnblogs.com/hyf-huangyongfei/p/12849406.html

https://www.codercto.com/a/52970.html

https://github.com/spring-cloud/spring-cloud-gateway/issues/1658

https://blog.csdn.net/zhangzhen02/article/details/109082792

关注我

个人公众号:后端技术漫谈



喜欢的朋友记得点赞、收藏、关注哦!!!

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

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

相关文章

深度学习:开启人工智能的新纪元

深度学习&#xff1a;开启人工智能的新纪元 深度学习是机器学习的一个子领域&#xff0c;它基于人工神经网络的学习算法&#xff0c;特别是那些具有多个非线性变换的层&#xff08;即“深度”&#xff09;。这些算法模仿人脑处理信息的方式&#xff0c;通过学习数据的多层次表…

Python Appium自动化操作抖音

1、功能介绍 使用Python和Appium给手机抖音上的同城模块自动评论&#xff0c;主要是通过模拟用户在抖音同城模块的操作&#xff0c;实现自动发送评论的功能。具体步骤如下&#xff1a; - 安装并配置好Python环境&#xff1b; - 安装Appium库&#xff0c;用于自动化操作手机应…

AI与测试行业调研

业务方向及应用场景 方向 技术 应用 大语言模型 私有化大模型&#xff1a; llama2 privateGPT 业务分析 测试数据生成 机器学习、深度学习应用 视觉自动化&#xff1a; FastbotApplitools 视觉自动化 缺陷预测与挖掘 知识图谱 neo4j 测试用例生成 精准测试 大语言模…

从A到Z,一文通览Python所有内置函数:编程效率提升100%

Python中的内置函数&#xff0c;这些函数覆盖了从基本数据处理到高级编程功能的各种用途。下面&#xff0c;将逐一介绍每个函数的用途和提供相应的代码示例。 A abs()&#xff1a;返回数字的绝对值。 print(abs(-5)) # 输出: 5 aiter()&#xff1a;返回异步迭代器。 async…

【从零开始的LeetCode-算法】910. 最小差值 II

给你一个整数数组 nums&#xff0c;和一个整数 k 。 对于每个下标 i&#xff08;0 < i < nums.length&#xff09;&#xff0c;将 nums[i] 变成 nums[i] k 或 nums[i] - k 。 nums 的 分数 是 nums 中最大元素和最小元素的差值。 在更改每个下标对应的值之后&#xf…

jmeter 从多个固定字符串中随机取一个值的方法

1、先新增用户参数&#xff0c;将固定值设置为不同的变量 2、使用下面的函数&#xff0c;调用这写变量 ${__RandomFromMultipleVars(noticeType1|noticeType2|noticeType3|noticeType4|noticeType5)} 3、每次请求就是随机取的值了

驾驶员异常行为检测数据集(猫脸码客 第223期)

驾驶员异常行为检测数据集 随着智能交通系统的快速发展&#xff0c;驾驶员危险驾驶行为检测已成为确保道路安全、减少交通事故的重要手段。通过先进的图像处理和机器学习技术&#xff0c;可以实现对驾驶员行为的实时监测和预警&#xff0c;从而有效遏制危险驾驶行为的发生。本…

基于SpringBoot设计模式之结构型设计模式·桥接模式

文章目录 介绍开始架构图定义类的功能定义类的实现 测试样例 总结 介绍 将抽象部分与它的实现部分分离&#xff0c;使他们都可以独立地发生变化。 Bridge的意思是桥梁。就像在现实世界中&#xff0c;桥梁的功能是将河流的两侧连接起来一样, Bridge模式的作用也是将两样东西连接…

FPGA图像处理之中值滤波

文章目录 一、什么是中值滤波&#xff1f;二、均值滤波和中值滤波对比三、FPGA实现3.1 Verilog代码3.2 仿真验证 一、什么是中值滤波&#xff1f; 在前一篇《FPGA图像处理之均值滤波》中&#xff0c;我们了解到了图像处理中常遇到的一些噪声类型以及均值滤波的原理以及实现。我…

【Linux】实现倒计时、进度条、gdb

文章目录 缓冲区1.概念2.作用3.刷新策略4.缓冲区位置 实现倒计时实现进度条Linux调试器----gdb 缓冲区 1.概念 缓冲区是计算机内存的一部分&#xff0c;用于暂时存储数据。它在数据传输过程中起到一个缓冲桥梁的作用&#xff0c;帮助协调数据传输的速度差异。缓冲区可以是磁盘…

魔百和 HG680-MC免拆 2+8免拆机卡刷固件

HG680MC免拆完美bin卡刷24年10月21日版本【修复语音、ADB端口9999、开启ADB的Root权限】 更新特点&#xff1a; 1.更新开机桌面为SMART动画&#xff1b; 2.安装app自动安装&#xff0c;无需点击确认、下一步等提示按钮内容; 3.ADB端口号为9999&#xff1b; 4.ADB权限为完美RO…

wireshark 解密浏览器https数据包

一、导出浏览器证书有两种方法 1、在浏览器快捷方式追加启动参数&#xff1a; --ssl-key-log-file"d:\log\2.log" C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe --ssl-key-log-file"d:\log\2.log" 2、环境变量中新建用…

SpringbootSpringcloud面试题(含代码及展示图)

1 简单说一下springboot? Spring Boot是由Pivotal团队提供的全新框架&#xff0c;其设计目的是用来简化Spring应用的初始搭建以及开发过程。 -使用springboot以后,搭建一个spring应用和开发&#xff0c;部署变得很简单. 2 怎么简单实现一个springboot应用 1&#xff09;新建ma…

SpringBoot获取bean的几种方式

目录 一、BeanFactory与ApplicationContext的区别 二、通过BeanFactory获取 三、通过BeanFactoryAware获取 四、启动获取ApplicationContext 五、通过继承ApplicationObjectSupport 六、通过继承WebApplicationObjectSupport 七、通过WebApplicationContextUtils 八、通…

Chrome DevTools 三: Performance 性能面板扩展—— 性能优化

Performance 性能 &#xff08;一&#xff09;性能指标 首次内容绘制 (First Contentful Paint&#xff0c;FCP)&#xff1a; 任意内容在页面上完成渲染的时间 最大内容绘制 (Largest Contentful Paint&#xff0c;LCP)&#xff1a; 最大内容在页面上完成渲染的时间 第一字节…

创客项目秀|基于XIAO ESP32C3的本地个人助理Mr.M

作者&#xff1a;Matthew Yu 来自&#xff1a;Fab academy 在数字化时代的浪潮中&#xff0c;柴火创客空间作为创新与实践的摇篮&#xff0c;不仅为Fab Academy 2024的学员们提供了一个充满活力的学习和创作环境&#xff0c;更是将科技的力量与人文关怀深度融合。今天&#x…

Redis——事务

文章目录 Redis 事务Redis 的事务和 MySQL 事务的区别:事务操作MULTIEXECDISCARDWATCHUNWATCHwatch的实现原理 总结 Redis 事务 什么是事务 Redis 的事务和 MySQL 的事务 概念上是类似的. 都是把⼀系列操作绑定成⼀组. 让这⼀组能够批量执行 Redis 的事务和 MySQL 事务的区别:…

【Next.js 项目实战系列】07-分配 Issue 给用户

原文链接 CSDN 的排版/样式可能有问题&#xff0c;去我的博客查看原文系列吧&#xff0c;觉得有用的话&#xff0c;给我的库点个star&#xff0c;关注一下吧 上一篇【Next.js 项目实战系列】06-身份验证 分配 Issue 给用户 本节代码链接 Select Button​ # /app/issues/[i…

【Java】正则表达式详解

目录 引言 一、基本概念 1.1 元字符 1.2 预定义字符类 1.3 边界匹配符 1.4 数量标识符 1.5 捕获与非捕获分组 二、Java中的正则表达式支持 三、正则表达式的使用示例 3.1 匹配字符串 3.2 替换字符串 3.3 分割字符串 3.4 使用Pattern和Matcher 3.5 捕获组和后向…

【电商项目】1分布式基础篇

1 项目简介 1.2 项目架构图 1.2.1 项目微服务架构图 1.2.2 微服务划分图 2 分布式基础概念 3 Linux系统环境搭建 查看网络IP和网关 linux网络环境配置 补充P123&#xff08;修改linux网络设置&开启root密码访问&#xff09; 设置主机名和hosts映射 主机名解析过程分析&…