SpringBoot
关于雪花算法ID问题
Redis 缓存穿透、缓存击穿、缓存雪崩
分布式锁与单服务事务
分布式锁与分布式事务
SpringBoot 返回 BigDecimal ,数值太小导致返回科学计数法
本文档使用 MrDoc 发布
-
+
首页
分布式锁与单服务事务
你想要的是 SpringBoot 框架下**分布式锁**与**单服务事务**结合使用的完整示例,核心是理解如何在分布式场景下,用分布式锁保证并发安全,同时用单服务事务保证数据操作的原子性(比如库存扣减、订单创建这类典型场景)。 ### 场景说明 以**商品库存扣减**为例: - 单服务事务:保证“扣减库存 + 生成订单”的操作要么都成功,要么都失败(原子性)。 - 分布式锁:解决多实例部署时,多个服务节点同时扣减同一个商品库存的并发问题(避免超卖)。 ### 一、环境准备 #### 1. 引入依赖(pom.xml) 选择 Redis + Redisson 实现分布式锁(Redisson 封装了 Redis 分布式锁的最佳实践,如自动续期、原子释放),同时引入 Spring 事务相关依赖: ```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> <!-- Lombok(简化代码) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> ``` #### 2. 配置文件(application.yml) 配置数据源、Redis、Redisson: ```yaml spring: # 数据源配置 datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # Redis 配置(Redisson 会复用此配置) data: redis: host: localhost port: 6379 password: # 无密码则留空 database: 0 # Redisson 额外配置(可选,默认即可) redisson: lock: watch-dog-timeout: 30000 # 锁的自动续期时间(30秒) ``` #### 3. 初始化数据库表 创建库存表和订单表,用于演示事务和分布式锁: ```sql -- 库存表 CREATE TABLE `product_stock` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint NOT NULL COMMENT '商品ID', `stock_num` int NOT NULL COMMENT '库存数量', PRIMARY KEY (`id`), UNIQUE KEY `uk_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `product_id` bigint NOT NULL COMMENT '商品ID', `buy_num` int NOT NULL COMMENT '购买数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试数据(商品ID=1,库存=100) INSERT INTO `product_stock` (`product_id`, `stock_num`) VALUES (1, 100); ``` ### 二、代码实现 #### 1. Redisson 配置类(可选,默认自动配置足够) 如果需要自定义 Redisson 配置,可添加此类: ```java import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); // 单机 Redis(集群/哨兵可参考 Redisson 官方文档) config.useSingleServer() .setAddress("redis://localhost:6379") .setPassword("") // 无密码则留空 .setDatabase(0); return Redisson.create(config); } } ``` #### 2. 业务层(核心:分布式锁 + 事务) **关键原则**:分布式锁要在**事务之外**获取(先锁后执行事务),避免事务未提交但锁已释放导致并发问题;锁的释放必须放在 `finally` 中,确保异常时也能释放。 ```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.UUID; import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor public class StockService { private final RedissonClient redissonClient; private final JdbcTemplate jdbcTemplate; /** * 扣减库存并创建订单(分布式锁 + 事务) * @param productId 商品ID * @param buyNum 购买数量 * @return 是否成功 */ public boolean deductStock(Long productId, Integer buyNum) { // 1. 定义分布式锁的key(按商品ID粒度,避免锁粒度太大) String lockKey = "stock:lock:" + productId; RLock lock = redissonClient.getLock(lockKey); try { // 2. 获取分布式锁(等待10秒,持有30秒,Redisson会自动续期) boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (!locked) { // 获取锁失败,返回并发失败 return false; } // 3. 执行核心业务(包含事务) return doDeductStockWithTransaction(productId, buyNum); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { // 4. 释放锁(必须在finally中,且仅释放自己持有的锁) if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * 单服务事务:扣减库存 + 创建订单(原子操作) */ @Transactional(rollbackFor = Exception.class) // 所有异常都回滚 public boolean doDeductStockWithTransaction(Long productId, Integer buyNum) { // 第一步:查询库存(加行锁,避免幻读) Integer stock = jdbcTemplate.queryForObject( "SELECT stock_num FROM product_stock WHERE product_id = ? FOR UPDATE", Integer.class, productId ); // 库存不足,直接返回失败 if (stock == null || stock < buyNum) { return false; } // 第二步:扣减库存 jdbcTemplate.update( "UPDATE product_stock SET stock_num = stock_num - ? WHERE product_id = ?", buyNum, productId ); // 第三步:创建订单(模拟业务,生成唯一订单号) String orderNo = UUID.randomUUID().toString().replace("-", ""); jdbcTemplate.update( "INSERT INTO order_info (order_no, product_id, buy_num) VALUES (?, ?, ?)", orderNo, productId, buyNum ); // 模拟异常(可取消注释测试事务回滚) // if (true) { // throw new RuntimeException("模拟业务异常,事务回滚"); // } return true; } } ``` #### 3. 控制器(测试接口) ```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; @RestController @RequiredArgsConstructor public class StockController { private final StockService stockService; /** * 测试接口:扣减库存 * 示例请求:http://localhost:8080/deductStock?productId=1&buyNum=1 */ @GetMapping("/deductStock") public String deductStock( @RequestParam Long productId, @RequestParam Integer buyNum ) { boolean success = stockService.deductStock(productId, buyNum); return success ? "库存扣减成功" : "库存不足或并发冲突"; } } ``` ### 三、关键代码解释 1. **分布式锁的获取与释放**: - `lock.tryLock(10, 30, TimeUnit.SECONDS)`:最多等待10秒,获取锁后持有30秒;Redisson 的 `RLock` 会自动续期(看门狗机制),避免事务执行时间超过锁过期时间。 - `finally` 中释放锁:确保无论业务是否异常,锁都能释放;`lock.isHeldByCurrentThread()` 避免释放其他线程持有的锁。 2. **单服务事务**: - `@Transactional(rollbackFor = Exception.class)`:指定所有异常都触发回滚(默认仅回滚运行时异常)。 - `SELECT ... FOR UPDATE`:查询库存时加行锁,避免其他事务同时修改该商品库存(解决单库并发问题)。 - 事务内的“扣减库存 + 创建订单”:要么都成功,要么都回滚(比如创建订单失败时,库存会回滚)。 3. **锁粒度**: - 锁Key为 `stock:lock:{productId}`,只锁定单个商品,而非全局锁,提升并发性能。 ### 四、测试验证 1. **单实例测试**: - 启动服务,多次访问 `http://localhost:8080/deductStock?productId=1&buyNum=1`,库存会逐步减少,订单表会新增记录; - 取消业务层的“模拟异常”注释,访问接口后,库存不会扣减(事务回滚),订单也不会创建。 2. **分布式测试**: - 启动多个服务实例(如端口8080、8081),用压测工具(如JMeter)同时访问两个实例的接口,最终库存不会超卖(分布式锁保证)。 ### 总结 1. **核心逻辑**:先通过 Redisson 分布式锁保证多实例并发安全,再通过 `@Transactional` 保证单服务内数据操作的原子性。 2. **关键注意点**: - 分布式锁必须在事务**外部**获取,避免事务未提交但锁已释放; - 锁的粒度要精细(按商品ID),避免全局锁降低并发; - 锁的释放必须放在 `finally` 中,且仅释放当前线程持有的锁。 3. **事务补充**:`SELECT ... FOR UPDATE` 是单库行锁,配合分布式锁可解决“分布式+单库”的双重并发问题。
admin
2026年1月6日 17:07
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码