@Transactional 事务,并发
可以防止并发插入吗先说结论 ✅单纯加一个Transactional事务不能防止并发插入重复数据很多人踩这个大坑。原理事务只保证ACID原子、一致、隔离、持久。 事务 ≠ 锁事务只是把一堆 SQL 打包要么全成要么全回滚。举个场景Transactional public void addUser(String name){ // 查询这个名字是否存在 User u mapper.selectByName(name); if(u null){ mapper.insert(name); } }两个请求同时进来同时查到数据库没有这个名字。 两个线程全部通过 if 判断然后先后执行插入。 最后插入两条重复数据哪怕加了 Transactional这就是著名的查询‑插入竞态Race Condition。事务拦不住。✅几种正确方案4 个等级1️⃣【最简单数据库兜底】唯一索引推荐必加给字段建唯一索引UNIQUE KEY uk_name (name)。 就算并发冲上来MySQL 直接抛出唯一键冲突异常不会产生脏数据。⚠️只是抛异常业务要捕获处理。2️⃣【悲观锁】select ... for updateselect * from user where name? for update;锁住这一行没有行就不会上锁所以这个对 “不存在的数据” 悲观锁无效 注意悲观锁防不住【新增不存在记录】的并发只适合更新已有数据。3️⃣【乐观锁】版本号 version 字段 适合更新不适合防止新增重复。4️⃣【分布式场景】Redis 分布式锁 / Zookeeper 锁。总结速记Transactional只管原子性不阻止并发插入重复。兜底永远交给数据库唯一索引这是最后一道防线。Java 代码层面只能减少概率不能 100% 保证。防并发插入完整示例场景按username不能重复防止并发插入重复账号重点Transactional解决不了查询‑插入竞态数据库唯一索引是兜底底线第一步建表加唯一索引必须做CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT 用户名, age INT, create_time DATETIME, -- 唯一索引数据库层面拦截重复最后一道防线 UNIQUE KEY uk_sys_user_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;方式 1Java 业务 捕获唯一索引冲突单机首选import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.dao.DuplicateKeyException; Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper userMapper; } /** * ❗注意就算加 Transactional也无法阻止线程同时查询为空 * Transactional 只保证内部SQL原子不会锁住查询逻辑 */ // Transactional 这里不需要事务单insert本身就是原子 public boolean addUser(String username, Integer age) { // 1.先查询这一步会出现竞态两个线程同时查到null User exist userMapper.selectByUsername(username); if(exist ! null){ return false; // 已存在 } try { User user new User(); user.setUsername(username); user.setAge(age); userMapper.insert(user); return true; } catch (DuplicateKeyException e) { // 并发场景别的线程抢先插入触发唯一索引冲突捕获异常返回重复 return false; } } }问题说明 两个线程同时执行selectByUsername都返回 null都会进入 try 块 其中一个先 insert 成功第二个 insert 直接抛DuplicateKeyException捕获返回 false。 查询判断只是前置校验不能依赖它做最终保证靠唯一索引兜底。方式 2数据库方案 —— INSERT … ON DUPLICATE KEYMySQL 特有直接一条 SQL 完成绕开 Java 查询竞态性能很好。 mapperinsert idinsertIfNotExist INSERT INTO sys_user(username,age) VALUES(#{username},#{age}) ON DUPLICATE KEY UPDATE idid; /insert不存在执行插入存在什么都不改更新自己 id可以通过 mybatis 返回值判断返回 1 插入成功返回 2 已存在。方式 3分布式环境多实例部署Redis 分布式锁单机的数据库唯一索引依旧保留再加分布式锁减少异常抛出。 伪代码public boolean addUserDistribute(String username,Integer age){ String lockKey lock:user: username; // 抢占锁 Boolean ok redisTemplate.opsForValue().setIfAbsent(lockKey,1, Duration.ofSeconds(10)); if(Boolean.FALSE.equals(ok)){ return false; // 获取锁失败正在被别的请求处理 } try{ // 锁内再执行查询插入逻辑 User exist userMapper.selectByUsername(username); if(exist ! null) return false; userMapper.insert(new User(username,age)); return true; }catch (DuplicateKeyException e){ return false; }finally { redisTemplate.delete(lockKey); //释放锁 } }⚠️分布式锁不能替代数据库唯一索引锁有可能超时、宕机释放失败数据库是最后的保险。关键面试记忆点Transactional不能防止并发插入重复事务不锁查询只保证一组 SQL 原子提交回滚。select if(not exist) insert这套代码本身就存在竞态并发下会重复插入。唯一索引是兜底必加无论上层代码怎么做。单机优先前置查询 捕获DuplicateKeyException追求性能用on duplicate key多实例再加分布式锁。补充悲观锁为什么对新增无效select * from sys_user where username ? for update;数据库没有这条记录for update加不上任何锁两个线程依旧同时查到空继续插入防不住新增重复。悲观锁适合更新已存在的数据不适合防新增。