SpringBoot
关于雪花算法ID问题
Redis 缓存穿透、缓存击穿、缓存雪崩
分布式锁与单服务事务
分布式锁与分布式事务
SpringBoot 返回 BigDecimal ,数值太小导致返回科学计数法
本文档使用 MrDoc 发布
-
+
首页
分布式锁与分布式事务
你现在想深入理解**分布式锁**和**分布式事务**的核心区别、联系,以及在 SpringBoot 中如何结合这两者解决分布式系统的核心问题——既要保证跨服务操作的原子性(分布式事务),又要保证关键资源的并发安全(分布式锁)。 ### 一、核心概念与核心区别 首先用一张表清晰区分两者的核心差异,这是理解的基础: | 维度 | 分布式锁 | 分布式事务 | |---------------------|-------------------------------------------|---------------------------------------------| | **核心目标** | 解决**分布式环境下多节点并发竞争同一资源**的问题,保证同一时间只有一个节点操作资源 | 解决**分布式环境下跨多个服务/数据库操作**的原子性问题,保证“要么全成,要么全败” | | **解决的核心问题** | 并发安全(如库存超卖、重复下单)| 数据一致性(如跨服务下单:创建订单+扣库存+扣余额,需全部成功/失败) | | **核心特性** | 排他性、可重入、超时释放、高可用 | 原子性、一致性、隔离性、持久性(ACID)| | **典型实现方案** | Redis(Redisson)、Zookeeper、MySQL行锁 | Seata(AT/TCC)、Saga、2PC/3PC、消息队列最终一致性 | | **适用场景** | 单个资源的并发修改(如扣减库存、抢红包)| 跨服务/跨库的多步操作(如订单+库存+支付)| | **失败后果** | 资源并发修改异常(超卖、重复写)| 数据不一致(如扣了库存但没创建订单)| ### 二、两者的关系:互补而非替代 - **分布式锁 ≠ 分布式事务**:锁只保证“同一时间只有一个操作”,但无法保证跨服务操作的原子性;事务只保证“多操作原子性”,但无法解决并发竞争问题。 - **结合使用场景**:在分布式事务中,对**核心资源(如库存)** 加分布式锁,既保证跨服务操作的原子性,又保证核心资源的并发安全。 ### 三、实战示例:跨服务下单(分布式事务 + 分布式锁) 以**跨服务下单场景**为例: - 涉及服务:订单服务(order-db)、库存服务(stock-db)、账户服务(account-db)。 - 核心需求: 1. 分布式事务:创建订单(订单库)+ 扣减库存(库存库)+ 扣减账户余额(账户库),三个操作要么全成,要么全败。 2. 分布式锁:库存扣减时加锁,避免多节点并发扣减导致超卖。 #### 1. 环境准备 ##### (1)核心依赖(pom.xml) 新增 Seata(分布式事务 AT 模式,最易上手)依赖,保留 Redisson 分布式锁依赖: ```xml <dependencies> <!-- SpringBoot 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JDBC + 事务 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Redisson 分布式锁 --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.3</version> </dependency> <!-- Seata 分布式事务 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2022.0.0.0-RC2</version> <!-- 排除自带的Seata版本,统一版本 --> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.0</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> ``` ##### (2)配置文件(application.yml) 配置 Seata、Redis、多数据源(简化为单应用模拟多服务,实际为多应用): ```yaml spring: application: name: seata-demo # Seata 事务组名称对应 # 数据源配置(模拟三个库:order、stock、account) datasource: # 订单库 order: url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 库存库 stock: url: jdbc:mysql://localhost:3306/stock_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 账户库 account: url: jdbc:mysql://localhost:3306/account_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # Redis 配置 data: redis: host: localhost port: 6379 password: database: 0 # Seata 配置 seata: enabled: true tx-service-group: seata-demo-group # 事务组名称 service: vgroup-mapping: seata-demo-group: default # 事务组对应注册中心的集群名 grouplist: default: 127.0.0.1:8091 # Seata Server 地址 config: type: file registry: type: file ``` ##### (3)Seata 服务端部署(简化) 1. 下载 Seata Server 1.7.0:https://github.com/seata/seata/releases 2. 修改 `conf/application.yml`,配置 registry(默认file即可)、store(默认file)。 3. 启动 Seata Server:`bin/seata-server.bat`(Windows)/ `bin/seata-server.sh`(Linux)。 ##### (4)数据库初始化 创建三个库及表,Seata AT 模式需每个库加 `undo_log` 表: ```sql -- 1. 订单库 order_db CREATE DATABASE IF NOT EXISTS order_db; USE order_db; -- 订单表 CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `product_id` bigint NOT NULL, `buy_num` int NOT NULL, `user_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Seata AT 模式undo_log表 CREATE TABLE `undo_log` ( `branch_id` bigint NOT NULL, `xid` varchar(128) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`branch_id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 库存库 stock_db CREATE DATABASE IF NOT EXISTS stock_db; USE stock_db; -- 库存表 CREATE TABLE `product_stock` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint NOT NULL, `stock_num` int NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试数据(商品1,库存100) INSERT INTO `product_stock` (`product_id`, `stock_num`) VALUES (1, 100); -- undo_log表(同上) CREATE TABLE `undo_log` ( `branch_id` bigint NOT NULL, `xid` varchar(128) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`branch_id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. 账户库 account_db CREATE DATABASE IF NOT EXISTS account_db; USE account_db; -- 账户表 CREATE TABLE `user_account` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `balance` decimal(10,2) NOT NULL COMMENT '余额', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试数据(用户1,余额1000) INSERT INTO `user_account` (`user_id`, `balance`) VALUES (1, 1000.00); -- undo_log表(同上) CREATE TABLE `undo_log` ( `branch_id` bigint NOT NULL, `xid` varchar(128) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`branch_id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 2. 代码实现 ##### (1)多数据源配置(模拟多服务) ```java import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; 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 DataSourceConfig { // 订单库数据源 @Bean("orderDataSource") @ConfigurationProperties("spring.datasource.order") public DataSource orderDataSource() { return DataSourceBuilder.create().build(); } // 库存库数据源 @Bean("stockDataSource") @ConfigurationProperties("spring.datasource.stock") public DataSource stockDataSource() { return DataSourceBuilder.create().build(); } // 账户库数据源 @Bean("accountDataSource") @ConfigurationProperties("spring.datasource.account") public DataSource accountDataSource() { return DataSourceBuilder.create().build(); } // 订单库JdbcTemplate @Bean("orderJdbcTemplate") public JdbcTemplate orderJdbcTemplate(@Qualifier("orderDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } // 库存库JdbcTemplate @Bean("stockJdbcTemplate") public JdbcTemplate stockJdbcTemplate(@Qualifier("stockDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } // 账户库JdbcTemplate @Bean("accountJdbcTemplate") public JdbcTemplate accountJdbcTemplate(@Qualifier("accountDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } } ``` ##### (2)库存服务(分布式锁 + 库存扣减) ```java import lombok.RequiredArgsConstructor; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor public class StockService { private final RedissonClient redissonClient; @Qualifier("stockJdbcTemplate") private final JdbcTemplate stockJdbcTemplate; /** * 扣减库存(分布式锁保证并发安全,本地事务保证单库操作原子性) */ @Transactional(rollbackFor = Exception.class) public boolean deductStock(Long productId, Integer buyNum) { // 1. 分布式锁:按商品ID加锁,避免多节点并发扣减 String lockKey = "dist:lock:stock:" + productId; RLock lock = redissonClient.getLock(lockKey); try { boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (!locked) { return false; } // 2. 查询库存(行锁,增强单库并发安全) Integer stock = stockJdbcTemplate.queryForObject( "SELECT stock_num FROM product_stock WHERE product_id = ? FOR UPDATE", Integer.class, productId ); if (stock == null || stock < buyNum) { return false; } // 3. 扣减库存 stockJdbcTemplate.update( "UPDATE product_stock SET stock_num = stock_num - ? WHERE product_id = ?", buyNum, productId ); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } } ``` ##### (3)账户服务(扣减余额) ```java import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.math.BigDecimal; @Service @RequiredArgsConstructor public class AccountService { @Resource(name = "accountJdbcTemplate") private final JdbcTemplate accountJdbcTemplate; /** * 扣减账户余额(本地事务) */ @Transactional(rollbackFor = Exception.class) public boolean deductBalance(Long userId, BigDecimal amount) { // 1. 查询余额 BigDecimal balance = accountJdbcTemplate.queryForObject( "SELECT balance FROM user_account WHERE user_id = ? FOR UPDATE", BigDecimal.class, userId ); if (balance == null || balance.compareTo(amount) < 0) { return false; } // 2. 扣减余额 accountJdbcTemplate.update( "UPDATE user_account SET balance = balance - ? WHERE user_id = ?", amount, userId ); return true; } } ``` ##### (4)订单服务(分布式事务入口 + 调用库存/账户服务) ```java import io.seata.spring.annotation.GlobalTransactional; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.UUID; @Service @RequiredArgsConstructor public class OrderService { @Resource(name = "orderJdbcTemplate") private final JdbcTemplate orderJdbcTemplate; private final StockService stockService; private final AccountService accountService; /** * 创建订单(分布式事务入口:@GlobalTransactional 保证跨服务原子性) */ @GlobalTransactional(rollbackFor = Exception.class) // Seata 分布式事务注解 public boolean createOrder(Long userId, Long productId, Integer buyNum, BigDecimal amount) { try { // 1. 扣减库存(跨服务调用,分布式锁保证并发) boolean stockSuccess = stockService.deductStock(productId, buyNum); if (!stockSuccess) { throw new RuntimeException("库存不足"); } // 2. 扣减账户余额(跨服务调用) boolean accountSuccess = accountService.deductBalance(userId, amount); if (!accountSuccess) { throw new RuntimeException("余额不足"); } // 3. 创建订单(本地库操作) String orderNo = UUID.randomUUID().toString().replace("-", ""); orderJdbcTemplate.update( "INSERT INTO order_info (order_no, product_id, buy_num, user_id) VALUES (?, ?, ?, ?)", orderNo, productId, buyNum, userId ); // 模拟异常(测试分布式事务回滚) // if (true) { // throw new RuntimeException("模拟异常,分布式事务回滚"); // } return true; } catch (Exception e) { // 抛出异常触发分布式事务回滚 throw new RuntimeException("创建订单失败:" + e.getMessage()); } } } ``` ##### (5)控制器(测试接口) ```java import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController @RequiredArgsConstructor public class OrderController { private final OrderService orderService; /** * 测试分布式事务+分布式锁 * 示例:http://localhost:8080/createOrder?userId=1&productId=1&buyNum=1&amount=10.00 */ @GetMapping("/createOrder") public String createOrder( @RequestParam Long userId, @RequestParam Long productId, @RequestParam Integer buyNum, @RequestParam BigDecimal amount ) { try { boolean success = orderService.createOrder(userId, productId, buyNum, amount); return success ? "订单创建成功" : "订单创建失败"; } catch (Exception e) { return "订单创建失败:" + e.getMessage(); } } } ``` ### 四、关键代码解释 1. **分布式事务核心**: - `@GlobalTransactional`:Seata AT 模式的核心注解,标记分布式事务的入口,Seata 会自动管理跨服务的事务分支,保证原子性。 - `undo_log` 表:Seata AT 模式用于记录数据修改前的快照,异常时自动回滚数据。 2. **分布式锁核心**: - 库存服务中的 `RLock`:保证多节点并发扣减同一商品库存时,只有一个节点能操作,避免超卖。 - 锁粒度:按 `productId` 加锁,保证精细粒度,提升并发。 3. **事务协同**: - 分布式事务保证“扣库存+扣余额+创订单”跨服务操作的原子性; - 分布式锁保证“扣库存”这个核心操作的并发安全; - 本地事务(`@Transactional`)保证单个服务内操作的原子性。 ### 五、测试验证 1. **正常场景**: 访问 `http://localhost:8080/createOrder?userId=1&productId=1&buyNum=1&amount=10.00`,订单库新增订单、库存库扣减库存、账户库扣减余额,三者都成功。 2. **异常场景**: 取消订单服务中“模拟异常”的注释,再次访问接口,Seata 会触发分布式事务回滚:订单不会创建、库存回滚、余额回滚,保证数据一致性。 3. **并发场景**: 用压测工具并发访问接口,分布式锁会保证库存不会超卖,即使多线程并发扣减,库存最终也不会为负。 ### 总结 1. **核心区别**: - 分布式锁:解决**分布式并发竞争资源**问题,保证“同一时间只有一个操作”; - 分布式事务:解决**跨服务操作原子性**问题,保证“要么全成,要么全败”。 2. **核心联系**: - 互补而非替代:分布式锁保证“操作的并发安全”,分布式事务保证“操作的原子性”; - 典型组合:跨服务操作中,对核心资源(如库存)加分布式锁,对跨服务操作加分布式事务。 3. **关键注意点**: - 分布式锁仍需在**本地事务外部**获取,避免锁提前释放; - Seata AT 模式是入门首选,无需侵入业务代码,适合大多数场景; - 分布式锁粒度要精细,避免全局锁降低并发性能。
admin
2026年1月6日 17:11
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码