SpringCloud2023中使用Seata解决分布式事务

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数据的操作要么一起成功,要么一起失败,必须是一个整体性的事务。Seata简化了这个使用过程。

你好,这里是codetrend专栏“SpringCloud2023实战”。

可以点击合集查看往期文章,这是第10篇更新。

本文简单介绍SpringCloud2023中集成Seata来使用分布式事务。

前言

对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数据的操作要么一起成功,要么一起失败,必须是一个整体性的事务。

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

简单的说,在分布式系统中一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。

任何事务机制在实现时,都应该考虑事务的 ACID 特性,包括:本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。

典型的分布式事务场景:跨库事务、分库分表、微服务化。

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,其内部版本在阿里系内部一直扮演着应用架构层数据一致性的中间件角色,帮助经济体平稳的度过历年的双11,对上层业务进行了有力的技术支撑。

启动 Seata Server

具体的详细教程可以查看seata文档,搜索seata即可。

  • 导入相关脚本。undo_log 、global_table、branch_table、lock_table、distributed_lock。
  • 下载seata server应用,启动 Seata server。

相关文件可以在seata的源码仓库获取到。

# linux系统
sh seata-server.sh -p $LISTEN_PORT -m $MODE(file or db) -h $HOST -e $ENV
# windows 系统
seata-server.bat -p $LISTEN_PORT -m $MODE(file or db) -h $HOST -e $ENV
  • Seata 1.5.1支持Seata控制台本地访问控制台地址:http://127.0.0.1:7091
  • 通过Seata控制台可以观察到正在执行的事务信息和全局锁信息,并且在事务完成时删除相关信息。

  • 修改seata server的启动配置,端口为7091,数据库用mysql,不适用注册中心。配置文件路径为 seata\conf\application.yml (可以参考配置 seata\conf\application.example.yml)。

server:
  port: 7091
spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${
   log.home:${
   user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata
seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: file
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: file
  store:
    # support: file 、 db 、 redis
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:20020/seata?rewriteBatchedStatements=true&useSSL=true
      user: root
      password: 123456
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      max-wait: 5000    
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login

Seata 集成之客户端

  • 需要三个应用以上才能进行演示,根据seata文档搭建account-server、order-service、storage-service、business-service这四个应用。
  • 数据库采用mysql,微服务组件选型还是使用之前确定的组件。
  • 更新: 由于seata支持zookeeper依赖的zkClient不支持jdk17(springboot3的最低要求),目前seata无法在zookeeper注册中心使用。通过如下配置直连seata server。
## 分布式事务相关配置
seata:
  enabled: true
  application-id: ${
   spring.application.name}
  tx-service-group: ${
   spring.application.name}-tx-group
  service:
    grouplist:
      default: 127.0.0.1:8091 # 需要是服务端端口,对应 seata.server.service-port
#    vgroup-mapping:
#      my_test_tx_group: seata-server # 此处配置对应Server端配置registry.eureka.application的值
  config:
    type: file
    file:
      name: "seata.properties"
  registry:
    type: file

引入pom.xml

  • 引入Seata 主要是引入 spring-cloud-starter-alibaba-seata
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>banana</artifactId>
        <groupId>io.rainforest</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>

    <artifactId>banana-seata-example</artifactId>
    <description>banana-seata-example 父工程</description>

    <modules>
        <module>account-server</module>
        <module>business-service</module>
        <module>order-service</module>
        <module>storage-service</module>
    </modules>

    <!-- 以下依赖 全局所有的模块都会引入  -->
    <dependencies>
        <!-- 引入分布式事务seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>
</project>

修改配置

  • 新增配置文件 application.yml,配置主要是 seata 下面的配置。
base:
  config:
    mdb:
      hostname: 127.0.0.1 #your mysql server ip address
      dbname: seata #your database name for test
      port: 20020 #your mysql server listening port
      username: 'root' #your mysql server username
      password: '123456' #your mysql server password

spring.application.name: account-service
spring:
  cloud:
    zookeeper:
      connect-string: localhost:2181
  datasource:
    name: storageDataSource
    #    druid don't support GraalVM now because of there is CGlib proxy
    #    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${
   base.config.mdb.hostname}:${
   base.config.mdb.port}/${
   base.config.mdb.dbname}?useSSL=false&serverTimezone=UTC
    username: ${
   base.config.mdb.username}
    password: ${
   base.config.mdb.password}
server:
  port: 10301
  servlet:
    context-path: /
# springdoc-openapi项目配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: io.rainforest.banana.app.web
# knife4j的增强配置,不需要增强可以不配
knife4j:
  enable: true
  setting:
    language: zh_cn
  basic:
    enable: true
    # Basic认证用户名
    username: yulin
    # Basic认证密码
    password: 123yl.

## 分布式事务相关配置
seata:
  enabled: true
  application-id: ${
   spring.application.name}
  tx-service-group: ${
   spring.application.name}-tx-group
  service:
    grouplist:
      default: 127.0.0.1:8091 # 需要是服务端端口,对应 seata.server.service-port
  config:
    type: file
    file:
      name: "seata.properties"
  registry:
    type: file

seata.properties 的相关配置:

service.vgroupMapping.order-service-tx-group=default
service.vgroupMapping.account-service-tx-group=default
service.vgroupMapping.business-service-tx-group=default
service.vgroupMapping.storage-service-tx-group=default

修改启动类

  • 启动类不需要特殊修改。
package io.rainforest.banana.client1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Application {
   
    public static void main(String[] args) {
   
        SpringApplication.run(Application.class, args);
    }
}

搭建演示应用

配置和pom依赖说明

  • application.yml 只有 server.port 有差异,其他配置相同,参考上述 修改配置 一节。
    • account-service 10301
    • business-service 10302
    • order-service 10303
    • storage-service 10304
  • pom.xml 全都相同,只有 artifactId 不一样,参考上述 引入pom.xml 一节。
  • 以下例子代码均来自 git/spring-cloud-alibaba/tree/2022.x/spring-cloud-alibaba-examples/seata-example ,部分例子代码单独编写。

account-server 搭建


@RestController
public class AccountControllerDemo {
   

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountControllerDemo.class);

    private static final String SUCCESS = "SUCCESS";

    private static final String FAIL = "FAIL";

    private final JdbcTemplate jdbcTemplate;

    private Random random;

    public AccountControllerDemo(JdbcTemplate jdbcTemplate) {
   
        this.jdbcTemplate = jdbcTemplate;
        this.random = new Random();
    }

    @PostMapping(value = "/account2", produces = "application/json")
    @GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
    public String account(String userId, int money) {
   
        LOGGER.info("Account Service ... xid: " + RootContext.getXID());

        int result = jdbcTemplate.update(
                "update account_tbl set money = money - ? where user_id = ?",
                new Object[] {
    money, userId });

        if (random.nextBoolean()) {
   
            throw new RuntimeException("this is a mock Exception");
        }

        LOGGER.info("Account Service End ... ");
        if (result == 1) {
   
            return SUCCESS;
        }
        return FAIL;
    }

}


@RestController
public class AccountControllerDemo {
   

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountControllerDemo.class);

    private static final String SUCCESS = "SUCCESS";

    private static final String FAIL = "FAIL";

    private final JdbcTemplate jdbcTemplate;

    private Random random;

    public AccountControllerDemo(JdbcTemplate jdbcTemplate) {
   
        this.jdbcTemplate = jdbcTemplate;
        this.random = new Random();
    }

    @PostMapping(value = "/account2", produces = "application/json")
    @GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
    public String account(String userId, int money) {
   
        LOGGER.info("Account Service ... xid: " + RootContext.getXID());

        int result = jdbcTemplate.update(
                "update account_tbl set money = money - ? where user_id = ?",
                new Object[] {
    money, userId });

        if (random.nextBoolean()) {
   
            throw new RuntimeException("this is a mock Exception");
        }

        LOGGER.info("Account Service End ... ");
        if (result == 1) {
   
            return SUCCESS;
        }
        return FAIL;
    }

}


package io.rainforest.banana.app.web.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

/**
 * 初始化账户数据
 */
@Configuration
public class DatabaseConfiguration {
   
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
   
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        jdbcTemplate.update("delete from account_tbl where user_id = 'U100001'");
        jdbcTemplate.update(
                "insert into account_tbl(user_id, money) values ('U100001', 10000)");

        return jdbcTemplate;
    }

}
  • 通过注解@GlobalTransactional开启事务控制。
  • 访问 http://localhost:10301/account2?userId=33&money=333 可以验证事务已开启成功。
  • 通过 seata server界面 http://localhost:7091/#/transaction/list 能够看到事务记录,账号密码 seata/seata 。

order-service 搭建


@RestController
public class OrderController {
   

    private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);

    private static final String SUCCESS = "SUCCESS";

    private static final String FAIL = "FAIL";

    private static final String USER_ID = "U100001";

    private static final String COMMODITY_CODE = "C00321";

    private final JdbcTemplate jdbcTemplate;

    private final RestTemplate restTemplate;

    private Random random;

    public OrderController(JdbcTemplate jdbcTemplate, RestTemplate restTemplate) {
   
        this.jdbcTemplate = jdbcTemplate;
        this.restTemplate = restTemplate;
        this.random = new Random();
    }

    @PostMapping(value = "/order", produces = "application/json")
    public String order(String userId, String commodityCode, int orderCount) {
   
        LOGGER.info("Order Service Begin ... xid: " + RootContext.getXID());

        int orderMoney = calculate(commodityCode, orderCount);

        invokerAccountService(orderMoney);

        final Order order = new Order();
        order.userId = userId;
        order.commodityCode = commodityCode;
        order.count = orderCount;
        order.money = orderMoney;

        KeyHolder keyHolder = new GeneratedKeyHolder();

        int result = jdbcTemplate.update(new PreparedStatementCreator() {
   

            @Override
            public PreparedStatement createPreparedStatement(Connection con)
                    throws SQLException {
   
                PreparedStatement pst = con.prepareStatement(
                        "insert into order_tbl (user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                        PreparedStatement.RETURN_GENERATED_KEYS);
                pst.setObject(1, order.userId);
                pst.setObject(2, order.commodityCode);
                pst.setObject(3, order.count);
                pst.setObject(4, order.money);
                return pst;
            }
        }, keyHolder);

        order.id = keyHolder.getKey().longValue();

        if (random.nextBoolean()) {
   
            throw new RuntimeException("this is a mock Exception");
        }

        LOGGER.info("Order Service End ... Created " + order);

        if (result == 1) {
   
            return SUCCESS;
        }
        return FAIL;
    }

    private int calculate(String commodityId, int orderCount) {
   
        return 2 * orderCount;
    }

    private void invokerAccountService(int orderMoney) {
   
        String url = "http://127.0.0.1:10301/account";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();

        map.add("userId", USER_ID);
        map.add("money", orderMoney + "");

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(
                map, headers);

        ResponseEntity<String> response = restTemplate.postForEntity(url, request,
                String.class);
    }

}


@Configuration
public class DatabaseConfiguration {
   
    @Bean
    public RestTemplate restTemplate() {
   
        return new RestTemplate();
    }
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
   
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        jdbcTemplate.execute("TRUNCATE TABLE order_tbl");

        return jdbcTemplate;
    }

}

package io.rainforest.banana.app.web.demo;

import java.io.Serializable;

public class Order implements Serializable {
   

    /**
     * id.
     */
    public long id;

    /**
     * user id.
     */
    public String userId;

    /**
     * commodity code.
     */
    public String commodityCode;

    /**
     * count.
     */
    public int count;

    /**
     * money.
     */
    public int money;

    @Override
    public String toString() {
   
        return "Order{" + "id=" + id + ", userId='" + userId + '\'' + ", commodityCode='"
                + commodityCode + '\'' + ", count=" + count + ", money=" + money + '}';
    }

}

storage-service 搭建


@Configuration
public class DatabaseConfiguration {
   


//  druid don't support GraalVM now because of there is CGlib proxy
    /*@Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSource storageDataSource() {
        return new DruidDataSource();
    }*/

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
   

        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        jdbcTemplate.update("delete from storage_tbl where commodity_code = 'C00321'");
        jdbcTemplate.update(
                "insert into storage_tbl(commodity_code, count) values ('C00321', 100)");

        return jdbcTemplate;

    }

}

@RestController
public class StorageController {
   

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageController.class);

    private static final String SUCCESS = "SUCCESS";

    private static final String FAIL = "FAIL";

    private final JdbcTemplate jdbcTemplate;

    public StorageController(JdbcTemplate jdbcTemplate) {
   
        this.jdbcTemplate = jdbcTemplate;
    }

    @GetMapping(value = "/storage/{commodityCode}/{count}", produces = "application/json")
    public String echo(@PathVariable String commodityCode, @PathVariable int count) {
   
        LOGGER.info("Storage Service Begin ... xid: " + RootContext.getXID());
        int result = jdbcTemplate.update(
                "update storage_tbl set count = count - ? where commodity_code = ?",
                new Object[] {
    count, commodityCode });
        LOGGER.info("Storage Service End ... ");
        if (result == 1) {
   
            return SUCCESS;
        }
        return FAIL;
    }

}

business-service 搭建


@RestController
public class HomeController {
   

    private static final Logger LOGGER = LoggerFactory.getLogger(HomeController.class);

    private static final String SUCCESS = "SUCCESS";

    private static final String FAIL = "FAIL";

    private static final String USER_ID = "U100001";

    private static final String COMMODITY_CODE = "C00321";

    private static final int ORDER_COUNT = 2;

    private final RestTemplate restTemplate;

    private final OrderService orderService;

    private final StorageService storageService;

    public HomeController(RestTemplate restTemplate, OrderService orderService,
            StorageService storageService) {
   
        this.restTemplate = restTemplate;
        this.orderService = orderService;
        this.storageService = storageService;
    }

    @GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
    @GetMapping(value = "/seata/rest", produces = "application/json")
    public String rest() {
   

        String result = restTemplate.getForObject(
                "http://127.0.0.1:10304/storage/" + COMMODITY_CODE + "/" + ORDER_COUNT,
                String.class);

        if (!SUCCESS.equals(result)) {
   
            throw new RuntimeException();
        }

        String url = "http://127.0.0.1:10303/order";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
        map.add("userId", USER_ID);
        map.add("commodityCode", COMMODITY_CODE);
        map.add("orderCount", ORDER_COUNT + "");

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(
                map, headers);

        ResponseEntity<String> response;
        try {
   
            response = restTemplate.postForEntity(url, request, String.class);
        }
        catch (Exception exx) {
   
            throw new RuntimeException("mock error");
        }
        result = response.getBody();
        if (!SUCCESS.equals(result)) {
   
            throw new RuntimeException();
        }

        return SUCCESS;
    }

    @GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
    @GetMapping(value = "/seata/feign", produces = "application/json")
    public String feign() {
   

        String result = storageService.storage(COMMODITY_CODE, ORDER_COUNT);

        if (!SUCCESS.equals(result)) {
   
            throw new RuntimeException();
        }

        result = orderService.order(USER_ID, COMMODITY_CODE, ORDER_COUNT);

        if (!SUCCESS.equals(result)) {
   
            throw new RuntimeException();
        }

        return SUCCESS;

    }

}

业务测试验证

场景说明

business-service 主业务服务提供一个场景,主要是如下两个接口,业务场景一致,分别使用openfeign和restTemplate调用。

  1. openfeign调用
  • 调用storage-service接口,扣减库存
  • 调用order-service接口,保存订单
  • order-service调用account-server接口,扣减账户余额
  1. restTemplate调用
  • 调用storage-service接口,扣减库存
  • 调用order-service接口,保存订单
  • order-service调用account-server接口,扣减账户余额

事务场景:

  • 库存服务扣减库存,如果库存不足或者保存异常,则抛出异常,回滚
  • 订单服务保存订单,如果订单保存失败,则抛出异常,回滚
  • 账户服务扣减账户余额,如果账户余额不足或者保存异常,则抛出异常,回滚

启动

通过控制台打印的 RootContext.getXID() 可以看出多个接口处于同一个事务。

关于作者

来自一线全栈程序员nine的探索与实践,持续迭代中。

欢迎关注、评论、点赞。

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
目录
相关文章
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
1天前
|
存储 SpringCloudAlibaba Java
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论。
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
|
17天前
|
消息中间件 SQL 中间件
大厂都在用的分布式事务方案,Seata+RocketMQ带你打破10万QPS瓶颈
分布式事务涉及跨多个数据库或服务的操作,确保数据一致性。本地事务通过数据库直接支持ACID特性,而分布式事务则需解决跨服务协调难、高并发压力及性能与一致性权衡等问题。常见的解决方案包括两阶段提交(2PC)、Seata提供的AT和TCC模式、以及基于消息队列的最终一致性方案。这些方法各有优劣,适用于不同业务场景,选择合适的方案需综合考虑业务需求、系统规模和技术团队能力。
127 7
|
29天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
51 6
|
29天前
|
数据库
如何在Seata框架中配置分布式事务的隔离级别?
总的来说,配置分布式事务的隔离级别是实现分布式事务管理的重要环节之一,需要认真对待和仔细调整,以满足业务的需求和性能要求。你还可以进一步深入研究和实践 Seata 框架的配置和使用,以更好地应对各种分布式事务场景的挑战。
29 6
|
27天前
|
消息中间件 运维 数据库
Seata框架和其他分布式事务框架有什么区别
Seata框架和其他分布式事务框架有什么区别
25 1
|
2月前
|
人工智能 文字识别 Java
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
尼恩,一位拥有20年架构经验的老架构师,通过其深厚的架构功力,成功指导了一位9年经验的网易工程师转型为大模型架构师,薪资逆涨50%,年薪近80W。尼恩的指导不仅帮助这位工程师在一年内成为大模型架构师,还让他管理起了10人团队,产品成功应用于多家大中型企业。尼恩因此决定编写《LLM大模型学习圣经》系列,帮助更多人掌握大模型架构,实现职业跃迁。该系列包括《从0到1吃透Transformer技术底座》、《从0到1精通RAG架构》等,旨在系统化、体系化地讲解大模型技术,助力读者实现“offer直提”。此外,尼恩还分享了多个技术圣经,如《NIO圣经》、《Docker圣经》等,帮助读者深入理解核心技术。
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
|
3月前
|
存储 NoSQL Redis
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
Redis持久化、RDB和AOF方案、Redis主从集群、哨兵、分片集群、散列插槽、自动手动故障转移
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
|
3月前
|
SQL NoSQL 数据库
SpringCloud基础6——分布式事务,Seata
分布式事务、ACID原则、CAP定理、Seata、Seata的四种分布式方案:XA、AT、TCC、SAGA模式
SpringCloud基础6——分布式事务,Seata
|
3月前
|
消息中间件 Java 对象存储
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
63 2

热门文章

最新文章