MySql悲观锁和乐观锁的使用

本文通过具体案例对比分析了乐观锁和悲观锁在并发控制中的应用,详细讲解了两种锁的实现方式及其在减库存场景下的表现,帮助读者理解不同场景下选择合适锁策略的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MySql悲观锁和乐观锁的使用

      该文转载自:

https://blog.youkuaiyun.com/yanghan1222/article/details/80449528

       现在我有一个购买商品的需求,我们知道当我们购买商品时,后台会进行减库存和增加购买记录的操作。我们分别在无锁和乐观锁和悲观锁进行相应的代码演示来说明问题。

    建表语句如下:

CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=981 DEFAULT CHARSET=utf8

一、无锁的Mysql:

    先看代码:


public class Test2 {
    public static void main(String[] args) throws SQLException, InterruptedException {
    Test2 tests = new Test2();
    Thread[] threads = new Thread[100];
    for (int i=0;i<100;i++){
    threads[i] = new Thread(){
        @Override
        public void run() {
        try {
        tests.service();
        }catch (Exception e){
        e.printStackTrace();
        }
        }
    };
    }
    for (int i=0;i<100;i++){
    threads[i].start();
    }
}

    public void service() throws Exception {
        Connection connection = dbUtils.getConnection();
        String selectSql = "select count from stock where id = 1";
        PreparedStatement statement1 = connection.prepareStatement(selectSql);
        ResultSet resultSet = statement1.executeQuery();
        resultSet.next();
        String count = resultSet.getString("count");
        System.out.println(count);
        int c = Integer.parseInt(count);
        Thread.sleep(10);
        if (c<1)
            throw new Exception();
        String updateSql = "update stock set count = count - 1 where count > 0";
        PreparedStatement preparedStatement = connection.prepareStatement(updateSql);
        int update = preparedStatement.executeUpdate();
        String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')";
        PreparedStatement statement = connection.prepareStatement(insertSql);
        int insert = statement.executeUpdate();
    }
}

    从上述代码可以看到,有一百个线程去模拟一百个用户购买商品,数据库中只有10个商品,所以当商品卖完时,应该增加10条购买记录。为了让大家看个清楚,我在代码中加入了线程的睡眠。

    

    我们看到,增加了11条记录,也就是所谓的超卖现象,商家绝不可能允许这种情况的发生。

MySql的乐观锁:

    我们在使用乐观锁时会假设在极大多数情况下不会形成冲突,只有在数据提交的时候,才会对数据是否产生冲突进行检验。如果数据产生冲突了,则返回错误信息,进行相应的处理。

    实现:MySql最经常使用的乐观锁时进行版本控制,也就是在数据库表中增加一列,记为version,当我们将数据读出时,将版本号一并读出,当数据进行更新时,会对这个版本号进行加1,当我们提交数据时,会判断数据库表中当前的version列值和当时读出的version是否相同,若相同说明没有进行更新的操作,不然,则取消这次的操作。

public class Test {
    public static void main(String[] args) {
    Test test = new Test();
    Thread[] threads = new Thread[200];
    for (int i=0;i<200;i++){
        int finalI = i;
        threads[i] = new Thread(){
            @Override
            public void run() {
            test.service();
            }
            };
    }
    for (int i=0;i<200;i++){
        threads[i].start();
    }
    }
    public void service(){
    try {
        Connection connection = dbUtils.getConnection();
        Stock stock1 = checkStock(connection);
        updateCountByOpti(connection,stock1);
        createOrder(connection);
    }catch (Exception e){
        System.out.println(e.getMessage());
    }
    }

    private void createOrder(Connection connection) throws SQLException {
        String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')";
        PreparedStatement statement = connection.prepareStatement(insertSql);
        int insert = statement.executeUpdate();
    }

    private void updateCountByOpti(Connection connection,Stock stock) throws SQLException {
    String sql = "update stock set count = count -1,version = version + 1 where version = " + stock.getVersion();
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    int update = preparedStatement.executeUpdate();
    if (update==0)
        throw new RuntimeException("没抢到");
    }

    public Stock checkStock(Connection connection) throws SQLException {
    String sql = "select * from stock where id = 1";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    ResultSet resultSet = preparedStatement.executeQuery();
    Stock stock = null;
    if (resultSet.next()){
        stock = new Stock();
        stock.setId(resultSet.getInt("id"));
        stock.setName(resultSet.getString("name"));
        stock.setCount(resultSet.getInt("count"));
        stock.setSale(resultSet.getInt("sale"));
        stock.setVersion(resultSet.getInt("version"));
    }

    if (stock.getCount()<1)
        throw new RuntimeException("没有库存了");
    return stock;
    }
}

    上述在提交时,对version字段进行了比较,当数据库中的version和之前读取的version一样才会进行提交,否则提交失败,接下来进行测试。

            

    可以看到,只有10条记录,乐观锁保证了数据的一致性。

三、悲观锁

    MySql的悲观锁就是打开事务,当启动事务时,如果事务中的sql语句涉及到索引并用索引进行了条件判断,那么会使用行级锁锁定所要修改的行,否则使用表锁锁住整张表。


public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        Thread[] threads = new Thread[200];
        for (int i=0;i<200;i++){
        threads[i] = new Thread(){
            @Override
            public void run() {
                try {
                test.service();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        };
        }
        for (int i=0;i<200;i++){
        threads[i].start();
        }
    }

    public void service() throws SQLException {
    Connection connection = null;
    try {
        connection = dbUtils.getConnection();
        connection.setAutoCommit(false);
        Stock stock1 = checkStock(connection);
        updateCountByOpti(connection,stock1);
        createOrder(connection);
        connection.commit();
    }catch (Exception e){
        System.out.println(e.getMessage());
        connection.rollback();
    }
    }
    private void createOrder(Connection connection) throws SQLException {
        String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')";
        PreparedStatement statement = connection.prepareStatement(insertSql);
        int insert = statement.executeUpdate();
    }
    private void updateCountByOpti(Connection connection,Stock stock) throws SQLException {
    String sql = "update stock set count = count -1,version = version + 1 where version = " + stock.getVersion();
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    int update = preparedStatement.executeUpdate();
    if (update==0)
        throw new RuntimeException("没抢到");
    }
    public Stock checkStock(Connection connection) throws SQLException, InterruptedException {
    String sql = "select * from stock where id = 1";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    ResultSet resultSet = preparedStatement.executeQuery();
    Stock stock = null;
    if (resultSet.next()){
        stock = new Stock();
        stock.setId(resultSet.getInt("id"));
        stock.setName(resultSet.getString("name"));
        stock.setCount(resultSet.getInt("count"));
        stock.setSale(resultSet.getInt("sale"));
        stock.setVersion(resultSet.getInt("version"));
    }

    if (stock.getCount()<1)
        throw new RuntimeException("没有库存了");
    return stock;
    }
}

    开启事务并不难,所以使用悲观锁很简单,让我们看一下结果

    结果还是10条记录

我们可以在不同的场合使用不同的处理方法,乐观锁并发高并且性能也很好,而悲观锁虽然并发不是很高,但是它不允许脏读,所以各有各的优点。

 

 

php+mysql使用乐观锁和悲观锁解决并发减库存问题

        本文转自:https://blog.youkuaiyun.com/resilient/article/details/81412145?utm_source=copy

        乐观锁和悲观锁看名称挺高大上的,面试的时候一些面试官最喜欢拿这个来考应聘者。这个也是有意义的,一个PHP程序员如果没有接触过乐观锁和非观锁,那么他根本没有接触过像样的业务了。

什么是悲观锁?看意思就是比较,不相信其它的人不会改,所以查询的时候就加锁,然后自己更新数据再释放锁。可以有效防止减库存冲突问题。

什么是乐观锁?相反就是认为没几个人用,基本不会碰到有人修改,所以查询时就不加锁,然后更新的时候判断一下数据是不是被改掉。

在php+mysql程序中怎么实现呢?代码也很简单。

悲观锁的业务流程(以商品表和SKU表减库存并进行其它操作为类):

---事务开始

1,查询商品表、锁定表:for update

2,判断商品库存是否大于购买数据,

3,如果库存满足,减少商品表库存。(不满足就回滚事务了。)

4,减少商品SKU表库存

5,记录订单操作记录等

--事务提交(事务提交时即释放锁)。

可见上面的流程中整个被锁的周期是比较长的。如果改为乐观锁呢:

----事务开始

1,查询商品表

 

2,判断商品库存是否大于购买数据,

3,如果库存满足,减少商品表库存(条件是商品表的库存=1中查询出的库存)

比如:update product set store = store - {$count} where store = {$product->store}  ($product->store为1查询得到的库存)

 

4,减少商品SKU表库存(同样需要判断条件)

5,记录订单操作记录等

--事务提交(事务提交时即释放锁)。

可以看到乐观锁整个过程中实际并未执行任何加锁,我也不知道为什么会有这样的称呼。实际它是一种判断冲突的有效手段。

在上面的乐观锁的执行流程中,如果3、4、5这三步中的任何一步发生异常,都会因滚事务。这样就不会出现减库存冲突导致库存脏数据了。

-------------------------------------------------------------

在使用乐观锁时,要考虑进一步,就是在乐观锁时如果发现数据被修改,更新失败时,要考虑再重新获取数据,重新判断重新更新。这样就不会因为更新失败导致此笔业务失败,而相当于把它立即加进到下一步的队列而在同步请求中即能得到解决。

两种锁各有各的好,建议电商网站起步时访问量不大,不会造成压力时使用悲观锁。因为这时没有什么高并发,但也要好好检查代码防止出现死锁。

对于成熟的电商网站,必须面对高并发的情况下,应该使用乐观锁。

MySQL 中的悲观锁乐观锁是两种处理并发访问数据一致性的机制,它们在实现方式、适用场景以及性能特点上有显著区别。 ### 悲观锁 悲观锁假设并发冲突经常发生,因此在整个数据处理过程中对数据保持锁定状态,以确保数据的完整性。悲观锁实现通常依赖于数据库提供的锁机制,例如行锁、读锁写锁等。这种锁机制在操作开始前就对数据加锁,直到操作完成才会释放锁。悲观锁适用于写入频繁且冲突概率较高的场景,例如金融交易系统,对数据一致性要求极高,无法容忍中间状态的错误。 在实际应用中,悲观锁通过 `SELECT ... FOR UPDATE` 或 `SELECT ... LOCK IN SHARE MODE` 等语句实现,确保数据在事务处理期间不被其他事务修改 [^3]。 ### 乐观锁 乐观锁则假设并发冲突较少,因此在数据操作时不立即加锁,而是在提交更新时检查是否有冲突发生。乐观锁实现通常依赖于版本号(Version)、时间戳(Timestamp)或比较交换(CAS)机制。如果在提交时检测到冲突,则拒绝操作并提示重试。这种锁机制适用于读取频繁且冲突概率较低的场景,例如统计数据的更新或低并发环境下的数据读取。 乐观锁的一个常见实现方式是通过版本号字段。在更新数据前,会检查当前版本号是否与数据库中的版本号一致,若一致则允许更新并增加版本号;否则,更新失败并提示冲突 [^2]。 ### 适用场景 - **悲观锁适用场景**: - **高冲突场景**:当系统中存在大量并发写入操作,冲突概率较高时,悲观锁可以有效避免数据不一致的问题。 - **数据敏感性高**:如金融系统中的交易处理,对数据一致性要求极高,不能容忍任何中间状态的错误。 - **实时性要求高**:需要立即获取数据并进行修改的场景,悲观锁可以确保数据在操作期间不会被其他事务修改 [^4]。 - **乐观锁适用场景**: - **低冲突场景**:当系统中读取操作远多于写入操作,且并发冲突概率较低时,乐观锁可以减少锁的开销,提高系统吞吐量。 - **允许重试**:在某些场景下,系统可以容忍操作失败并进行重试,此时乐观锁更为合适。 - **数据一致性要求适中**:对于某些非关键数据,如统计数据或日志信息,允许一定程度上的不一致,乐观锁可以提供更好的性能 [^2]。 ### 总结 悲观锁乐观锁各有优劣,选择合适的锁机制应根据具体的业务需求系统特点来决定。悲观锁适合高冲突、高敏感度的场景,而乐观锁则适合低冲突、高读取频率的场景。理解这两种锁机制的核心思想适用场景,有助于在实际开发中做出更合理的选择 [^4]。 ```sql -- 悲观锁示例:使用 SELECT ... FOR UPDATE 获取行锁 START TRANSACTION; SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE; -- 执行更新操作 UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; COMMIT; -- 乐观锁示例:使用版本号控制并发更新 UPDATE accounts SET balance = balance - 100, version = version + 1 WHERE account_id = 1 AND version = 5; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值