首页 新闻 会员 周边

synchronized 锁失效问题

0
悬赏园豆:20 [已解决问题] 解决于 2023-06-25 09:39

我们的系统遇到了秒杀问题。货主发布货源,司机抢单。在司机抢单的时候扣减承运车数,直到满足货主要求的需要的总车数。

这个方法添加了事务的注解。一开始出现过抢单数>扣减车数的问题,后来加上了synchronized同步锁。但是还是出现了抢单数和扣减车数不匹配的问题,扣减车数比抢单数量少。

通过查看运单的创建时间和接口访问时间分析发现这几次抢单接口的访问时间非常接近,毫秒级的接近。但是这个synchronized锁没生效,没能锁住方法,或者说没有实现正常扣减。

我想问问这是为什么呢?

系统是微服务,但是这个应用是单应用的,没有集群部署,肯定不会是多实例的问题。

我看网上有说是因为事务注解的问题,在上一次请求处理完,释放锁之后,本次请求就获取了已扣减数量,但是上一次事务没提交,导致本次请求实际上获取的是脏数据。不知道是不是这个原因呢?

如果是这个原因,又该怎么解决啊!
下面是代码:

具体扣减车数是:

LittleAnts的主页 LittleAnts | 初学一级 | 园豆:4
提问于:2023-06-24 17:40
< >
分享
最佳答案
0

当处理并发问题时,可以考虑使用乐观锁来处理已承运车数,以确保不受并发问题的影响。乐观锁是一种乐观的并发控制策略,它假设并发冲突的概率较低,并且在更新数据时检查数据的版本是否发生了变化。

以下是一个示例代码,展示了如何使用乐观锁来处理已承运车数:

// 获取当前运单的信息(包括已承运车数和版本号)
Plan plan = planService.getPlanById(planId);

// 检查运单是否已满足要求
if (plan.getPlan_total() <= plan.getPlan_f_total()) {
    plan.setPlan_state(plan_state.PAUSE.getValue());
    planService.updateIgnoreNull(plan);
    return; // 运单已满足要求,不需要再抢单
}

// 更新已承运车数和版本号
plan.setPlan_f_total(plan.getPlan_f_total() + 1);

// 使用乐观锁更新运单信息
int updatedRows = planService.updatePlanWithVersion(plan);

if (updatedRows == 0) {
    // 更新失败,说明运单在更新过程中被其他线程修改了
    // 可以进行重试或者进行其他处理
    // ...
} else {
    // 更新成功,继续进行抢单操作
    // ...
}

在上述示例代码中,我们首先获取了当前运单的信息,包括已承运车数和版本号。然后,我们检查运单是否已满足要求,如果已满足要求,则将运单状态设置为暂停,并更新到数据库中。

如果运单未满足要求,我们将使用乐观锁来更新已承运车数和版本号。updatePlanWithVersion 方法是一个自定义的更新方法,它会同时检查运单的版本号是否与之前获取的版本号一致。如果版本号一致,说明在更新过程中没有其他线程修改了运单信息,更新操作将成功。如果版本号不一致,说明运单在更新过程中被其他线程修改了,更新操作将失败。

通过使用乐观锁,我们可以在更新运单信息时检测并发冲突,并根据需要进行重试或进行其他处理。这样可以确保已承运车数不受并发问题的影响。

planService.updatePlanWithVersion(plan) 的方法实现:

public int updatePlanWithVersion(Plan plan) {
    // 获取数据库连接,并创建一个新的会话
    Connection conn = null;
    try {
        conn = getConnection();
        conn.setAutoCommit(false); // 设置手动提交事务

        // 查询当前运单的版本号
        int currentVersion = getCurrentVersion(conn, plan.getId());

        // 检查版本号是否一致,防止并发冲突
        if (currentVersion != plan.getVersion()) {
            throw new OptimisticLockException("Concurrent modification detected. Cannot update the plan.");
        }

        // 更新已承运车数和版本号
        int updatedRows = updatePlan(conn, plan.getId(), plan.getPlan_f_total(), currentVersion + 1);

        if (updatedRows == 0) {
            throw new OptimisticLockException("Concurrent modification detected. Cannot update the plan.");
        }

        // 提交事务
        conn.commit();

        return updatedRows;
    } catch (SQLException e) {
        try {
            if (conn != null) {
                conn.rollback(); // 回滚事务
            }
        } catch (SQLException rollbackEx) {
            // 忽略回滚异常
        }
        throw new RuntimeException("Failed to update the plan.", e);
    } finally {
        closeConnection(conn); // 关闭数据库连接
    }
}

// 查询当前运单的版本号
private int getCurrentVersion(Connection conn, int planId) throws SQLException {
    try (PreparedStatement stmt = conn.prepareStatement("SELECT version FROM plans WHERE id = ?")) {
        stmt.setInt(1, planId);
        try (ResultSet rs = stmt.executeQuery()) {
            if (rs.next()) {
                return rs.getInt("version");
            } else {
                throw new RuntimeException("Plan not found.");
            }
        }
    }
}

// 更新已承运车数和版本号
private int updatePlan(Connection conn, int planId, int planFTotal, int newVersion) throws SQLException {
    try (PreparedStatement stmt = conn.prepareStatement("UPDATE plans SET plan_f_total = ?, version = ? WHERE id = ? AND version = ?")) {
        stmt.setInt(1, planFTotal);
        stmt.setInt(2, newVersion);
        stmt.setInt(3, planId);
        stmt.setInt(4, newVersion - 1); // 使用旧版本号进行检查

        return stmt.executeUpdate();
    }
}

// 获取数据库连接
private Connection getConnection() throws SQLException {
    // 这里是获取数据库连接的逻辑,可以根据你的具体情况进行实现
    // 例如使用连接池来管理数据库连接
    // ...
}

// 关闭数据库连接
private void closeConnection(Connection conn) {
    // 关闭数据库连接的逻辑
    // ...
}

上述代码中,updatePlanWithVersion 方法使用了数据库事务来确保更新的原子性。首先,它获取数据库连接并设置手动提交事务。然后,通过查询当前运单的版本号,检查版本号是否与需要更新的版本号一致。如果不一致,说明运单在更新过程中被其他线程修改了,触发了乐观锁异常。

如果版本号一致,则使用预处理语句执行更新操作,同时将已承运车数和版本号更新为新的值。如果更新的行数为0,表示更新操作失败,再次触发乐观锁异常。

如果更新操作成功,则提交事务。如果在更新过程中发生异常,则执行回滚操作。最后,关闭数据库连接。

收获园豆:20
lanedm | 老鸟四级 |园豆:2381 | 2023-06-25 08:39

如果是数据库事务导致的抢单和扣减数不一致,乐观锁能锁住吗?如果上一次事务没提交但是这次已经运行到更新了那这次更新的结果条数是1还是0,事务提交之后结果会变吗?

LittleAnts | 园豆:4 (初学一级) | 2023-06-25 08:50

@LittleAnts: 如果使用乐观锁来实现并发控制,那么在数据库事务导致的抢单和扣减数不一致的情况下,乐观锁是可以锁住的。

当一个事务在更新数据之前先读取了数据,并且在更新时检查了数据的版本号,如果版本号发生了变化,表示有其他事务已经修改了数据,那么当前事务就会回滚并抛出一个异常(如 OptimisticLockException)。这样可以确保在并发修改的情况下,只有一个事务能够成功更新数据,其他事务需要重新读取数据并重新尝试更新。

对于你提到的情况,如果上一次事务没有提交但是当前事务已经运行到更新操作,那么更新的结果条数是0,因为上一次事务还未提交,所以当前事务无法获取到需要更新的数据。

当上一次事务提交后,下一次事务再次尝试更新时,更新的结果条数是1。因为上一次事务已经将数据更新到了数据库中,所以当前事务可以成功更新数据。

仔细看代码,乐观锁的机制是在事务开始时读取数据的版本号,并在事务提交前检查版本号是否发生变化。如果版本号发生变化,当前事务会回滚并抛出异常。因此,乐观锁能够有效地解决数据库事务导致的抢单和扣减数不一致的问题。

lanedm | 园豆:2381 (老鸟四级) | 2023-06-25 08:51

@lanedm: 如果在乐观锁更新方法中手动控制事务的开启,那是不是整个抢单方法的事务注解就不能用了呢

LittleAnts | 园豆:4 (初学一级) | 2023-06-25 09:25

@LittleAnts: 如果在乐观锁更新方法中手动控制事务的开启,那么整个抢单方法的事务注解将不会生效,因为抢单方法的事务注解通常是由框架或容器来管理的。在手动控制事务的情况下,你需要在抢单方法中手动处理事务的开启和提交。

在这种情况下,你可以将抢单方法标记为 @Transactional(propagation = Propagation.REQUIRES_NEW),以确保抢单方法在一个独立的事务中执行。然后,在乐观锁更新方法中手动控制事务的开启和提交,以确保乐观锁的并发控制生效。

lanedm | 园豆:2381 (老鸟四级) | 2023-06-25 09:36

@lanedm: 好的,学到了,谢谢

LittleAnts | 园豆:4 (初学一级) | 2023-06-25 09:38
其他回答(1)
0

在你描述的情况下,确实存在synchronized锁失效的问题。这可能与事务注解和多线程并发访问相关。

首先,让我们理解一下synchronized关键字的工作原理。synchronized关键字用于保护共享资源,确保在同一时间只有一个线程可以执行被锁定的代码块或方法。然而,对于分布式系统或多实例环境,synchronized无法跨越不同的JVM或服务器,因为每个实例都有自己的内存空间和锁。

在你的情况下,尽管你的应用是单应用的,但仍然存在多个并发线程同时访问同一个方法的可能性。在多线程环境下,synchronized关键字只能确保同一个实例内的线程安全,而不能跨多个线程或请求。

另一个可能的问题是事务注解的使用。如果你的事务注解被放置在不正确的位置,可能导致锁失效。例如,如果事务注解被放在方法内部的某个子方法上,而不是放在调用该方法的外部方法上,那么锁将无法正确应用。

为了解决这个问题,你可以考虑以下几点:

检查事务注解的使用:确保事务注解正确地应用在包含synchronized关键字的方法上,并覆盖需要保护的完整逻辑。

考虑使用更细粒度的锁:synchronized关键字只能锁定整个方法或代码块,如果在同一个方法中存在多个独立的锁定区域,可以考虑使用更细粒度的锁,例如使用ReentrantLock类来实现。

考虑并发数据结构:如果抢单数和扣减车数之间存在并发冲突,可以考虑使用并发数据结构,如ConcurrentHashMap或AtomicInteger等,来确保原子性操作和线程安全。

考虑分布式锁:如果你的系统需要在分布式环境下运行,可以考虑使用分布式锁来保证多个实例之间的互斥访问。

综上所述,确保事务注解正确应用并覆盖整个关键逻辑,并考虑使用更细粒度的锁或并发数据结构来解决并发冲突。如果你的系统需要在分布式环境下运行,还可以考虑使用分布式锁来解决锁失效的问题。

Technologyforgood | 园豆:5992 (大侠五级) | 2023-06-24 18:58

我想用redis的缓存来控制下总车数的扣减。抢单成功,如果不发生异常缓存数+1,用increment自增,如果异常了就将缓存数-1,这样每次抢单的时候都用缓存数和设置的总数比较。我们多个消费者和一个生产者共用的同一个redis缓存。

支持(0) 反对(0) LittleAnts | 园豆:4 (初学一级) | 2023-06-25 08:55
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册