菜鸟游戏网 - 游戏让生活变快乐! 全站导航 全站导航
AI工具安装教程 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

您的位置 : 资讯 > 其他资讯 > 别再只会加事务了:用 Java 从 0 构建高并发抢票系统,彻底吃透死锁与隔离级别

别再只会加事务了:用 Java 从 0 构建高并发抢票系统,彻底吃透死锁与隔离级别

来源:菜鸟下载 | 更新时间:2026-04-26

别再只会用 @Transactional:它并不能防并发问题 很多Ja va开发者遇到抢座、秒杀这类场景,第

别再只会用 @Transactional:它并不能防并发问题

很多Ja va开发者遇到抢座、秒杀这类场景,第一反应就是祭出@Transactional注解。代码写出来大概长这样:

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

public class ReservationService {
    @Transactional
    public void reserve(List seatIds, Long userId) {
        List seats = seatRepository.findAllById(seatIds);
        for (Seat seat : seats) {
            if (!"a vailable".equals(seat.getStatus())) {
                throw new RuntimeException("Seat not a vailable");
            }
        }
        for (Seat seat : seats) {
            seat.setStatus("reserved");
            seat.setReservedBy(userId);
            seat.setReservedUntil(LocalDateTime.now().plusMinutes(10));
        }
        seatRepository.sa veAll(seats);
    }
}

单线程测试,一切完美。可一旦上线,面对30万用户同时刷新页面抢8万个座位的真实场景,问题就全暴露了:同一个座位被卖了两次、接口超时、日志里刷屏的deadlock detected。问题出在哪?其实不是代码语法错了,而是对数据库在并发下的行为理解得还不够透。

别再忽略 MVCC:你读到的可能是“过去的数据”

这里有个关键认知需要刷新:无论是PostgreSQL还是MySQL的InnoDB引擎,默认都使用MVCC(多版本并发控制)。这意味着什么?简单说,一个普通的SELECT语句默认是不加锁的,你读到的是事务开始时的某个“快照”,而不是数据库当前最新的状态。尤其在READ COMMITTED隔离级别下,每条语句看到的快照都可能不同。事务保证了原子性(要么全成功,要么全失败),但它可不保证你读到的数据是最新的。

并发问题复现

图片

上图清晰地展示了并发场景下,两个事务如何因为读到旧的“可用”状态,导致超卖。你以为的“查询-判断-更新”安全流程,在并发下不堪一击。

别再忽略真正解决方案:悲观锁(行锁)

要解决这类“先查后改”的并发竞争,最直接有效的方法就是使用数据库的行级锁。思路很明确:在查询座位状态的那一刻,就直接把目标行锁住,让其他事务排队等待,从源头上杜绝冲突。

正确写法(JPA + SQL)

具体到Ja va(Spring Data JPA)中,可以这样实现:

@Repository
public interface SeatRepository extends JpaRepository {
    @Query(value = """
        SELECT * FROM seats 
        WHERE id IN (:ids)
        ORDER BY id
        FOR NO KEY UPDATE
    """, nativeQuery = true)
    List lockSeats(@Param("ids") List ids);
}

@Service
public class ReservationService {
    private static final int HOLD_MINUTES = 10;

    @Transactional
    public void reserve(List seatIds, Long userId) {
        List sortedIds = seatIds.stream()
                .distinct()
                .sorted()
                .toList();

        List seats = seatRepository.lockSeats(sortedIds);
        if (seats.size() != sortedIds.size()) {
            throw new RuntimeException("Seat not found");
        }

        for (Seat seat : seats) {
            if (!"a vailable".equals(seat.getStatus())) {
                throw new RuntimeException("Seat already taken");
            }
        }

        LocalDateTime expireTime = LocalDateTime.now().plusMinutes(HOLD_MINUTES);
        seatRepository.batchUpdateReserve(sortedIds, userId, expireTime);
    }
}

别再忽略三个关键细节(决定系统生死)

(1)必须排序加锁(否则必死锁)

注意代码里的.sorted()和SQL里的ORDER BY id。这可不是为了好看,而是避免死锁的生命线。死锁的本质就是多个事务以不同的顺序请求锁资源,形成了循环等待。强制所有事务都按相同的顺序(比如ID升序)加锁,就能从根本上打破这个循环。

图片图片

(2)优先使用 FOR NO KEY UPDATE

在PostgreSQL中,FOR UPDATEFOR NO KEY UPDATE有细微但重要的区别。后者锁的粒度更小,它阻止其他事务修改该行,但允许其他事务以FOR KEY SHARE的方式读取(这通常不影响外键引用)。在“只修改状态字段,不修改主键或唯一索引字段”的场景下,使用FOR NO KEY UPDATE可以提高并发度。

(3)事务必须极短

锁的持有时间直接决定系统的吞吐量。因此,被@Transactional包裹的方法里,应该只包含最核心的数据库操作:加锁查询、业务校验、执行更新。像HTTP调用、RPC、支付接口这些耗时操作,务必在释放锁(即事务提交)之后再执行。否则,锁长时间不释放,系统很快就会陷入瓶颈。

别再只会悲观锁:乐观锁同样重要

悲观锁是“先锁再改”,适合冲突频繁的场景。但如果冲突不那么频繁,乐观锁“先改再验”的模式往往性能更高。

方式一:version 字段

利用JPA的@Version注解实现乐观锁:

@Entity
public class Seat {
    @Id
    private Long id;
    @Version
    private Integer version;
    private String status;
}
// 更新时
seat.setStatus("reserved");
seatRepository.sa ve(seat); // JPA会自动在UPDATE语句中带上 version = ? 条件

如果更新时发现版本号对不上,JPA会抛出OptimisticLockException,这时业务层进行重试或提示即可。

方式二:条件更新(性能更高)

直接使用一条UPDATE语句,以状态作为更新条件,这是性能最高的方式:

@Modifying
@Query("""
UPDATE Seat s SET s.status = 'reserved',
    s.reservedBy = :userId,
    s.reservedUntil = :expireTime
WHERE s.id IN :ids AND s.status = 'a vailable'
""")
int updateA vailableSeats(...);

业务逻辑判断:

int updated = repository.updateA vailableSeats(ids, userId, expireTime);
if (updated != ids.size()) {
    throw new RuntimeException("部分座位已被占用");
}

这种方式优点是无锁、单条SQL、性能极致。缺点是需要处理部分更新成功的情况,通常需要引入补偿逻辑(如释放已锁定的座位)。

别再混淆隔离级别:它解决的是另一类问题

隔离级别主要解决“读”的一致性问题,而不是“写”的并发冲突。

READ COMMITTED(默认)

每条语句看到的是最新已提交的数据。这可能导致“不可重复读”和“幻读”,在复杂的业务逻辑中产生错误。

REPEATABLE READ

整个事务期间看到的数据快照是一致的。它能防止同一行数据的更新冲突,但对于“幻读”(范围查询中新增的行),InnoDB通过间隙锁在一定程度上解决,PostgreSQL则可能无法完全避免。

将隔离级别设为SERIALIZABLE是最严格的,它通过强制事务串行化执行来避免所有并发问题,但代价是性能最低,且事务可能因冲突而回滚,必须配套重试机制。

@Transactional(isolation = Isolation.SERIALIZABLE)

别再等线上才遇到死锁

一个典型的错误写法是:

SELECT * FROM seats WHERE id IN (...) FOR UPDATE

问题在于,数据库对IN (...)子句中的ID加锁顺序是不确定的。如果两个事务传入的ID列表顺序不同,就极有可能形成死锁。正确的写法前面已经强调:务必加上ORDER BY id

SELECT * FROM seats WHERE id IN (...) ORDER BY id FOR NO KEY UPDATE

别再只写代码:系统架构才是关键

图片

数据库锁只是最后一道防线。一个健壮的高并发系统,需要多层架构共同保障:

核心策略

读写分离:将查询流量导向只读副本,减轻主库压力。
Redis限流:在网关或应用层对用户请求进行限流和排队,避免流量洪峰直接冲击数据库。
连接池优化:使用HikariCP等高效连接池,对于PostgreSQL可配合PgBouncer减少连接开销。
事务精简:再次强调,事务内绝不进行外部调用。

别再写 Demo:一份可上线的 Ja va 实现

将上述所有要点整合,一个相对完整的服务层实现如下:

@Service
public class ReservationService {
    private static final int MAX_SEATS = 6;
    private static final int HOLD_MINUTES = 10;

    @Transactional
    public ReservationResponse reserve(List seatIds, Long userId) {
        List ids = seatIds.stream()
                .distinct()
                .sorted()
                .toList();

        if (ids.isEmpty() || ids.size() > MAX_SEATS) {
            throw new IllegalArgumentException("Invalid seat count");
        }

        List seats = seatRepository.lockSeats(ids);
        if (seats.size() != ids.size()) {
            throw new RuntimeException("Seat not found");
        }

        for (Seat seat : seats) {
            if (!"a vailable".equals(seat.getStatus())) {
                throw new RuntimeException("Seat already taken");
            }
        }

        LocalDateTime expire = LocalDateTime.now().plusMinutes(HOLD_MINUTES);
        seatRepository.batchUpdateReserve(ids, userId, expire);

        return new ReservationResponse(ids, expire);
    }
}

别再忽略项目结构

清晰的代码组织有助于长期维护:

/src/main/ja va/com/icoderoad/
    reservation/   # 应用服务层
    domain/        # 领域模型与仓储接口
    infrastructure/# 基础设施(持久化实现等)

对应的表结构建议:

CREATE TABLE seats (
  id BIGSERIAL PRIMARY KEY,
  event_id BIGINT NOT NULL,
  section VARCHAR(10),
  row_label VARCHAR(5),
  number INT,
  status VARCHAR(20) DEFAULT 'a vailable',
  reserved_by BIGINT,
  reserved_until TIMESTAMP,
  version INT DEFAULT 0
);

说到底,大多数系统在高并发下崩溃,根源往往不是业务逻辑有多复杂,而是低估了并发的复杂性。我们容易陷入一个误区:以为事务能兜住所有底,但它其实只保证“同时失败”;以为数据库会自动处理好冲突,但它更多只是在忠实地记录冲突。构建真正可靠的高并发系统,其核心能力在于:即使面对混乱无序的竞争,也能通过严谨的设计,让最终结果保持绝对正确。

菜鸟下载发布此文仅为传递信息,不代表菜鸟下载认同其观点或证实其描述。

展开
ja
ja
类型:街机游戏 运营状态:公测 语言:简体中文
模拟器
前往下载

相关文章

更多>>

热门游戏

更多>>