我是靠谱客的博主 眯眯眼黄豆,最近开发中收集的这篇文章主要介绍多线程执行数据库MYSQL扣减库存问题分析与解决方案问题一:超卖问题二 死锁问题:通过redis如果redis挂了怎么办?,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

由于本人菜鸡,没接触过太大的并发量,只用过Redis那一套(Redis的写性能和读性能都远高于MySQL)

多线程对数据库中的库存进行修改扣减的时候,会出现超卖或是死锁情况,如下:

问题一:超卖

当多个线程执行select然后再进行update时,由于mysql的select并未进行加锁,导致查询出的库存信息与实际的库存数量不一致,导致update时候一直扣减到了负数。

package com.example.service;

import java.sql.*;
import java.util.Random;

/**
 * 模拟多线程下并发扣减库存问题
 */
public class KuCunTest {


    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        Object object = new Object();
        for (int i = 0; i < 100; i++) {
            //初始情况多线程进行竞争 会导致 超卖以及死锁
            new ThreadChaoMai(false).start();

        }
    }

    // 超卖情况
    private static class ThreadChaoMai extends Thread {

        private boolean testDeadLock;

        ThreadChaoMai(boolean testDeadLock) {
            this.testDeadLock = testDeadLock;
        }

        @Override
        public void run() {
            try {
                Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
                connection.setAutoCommit(false);
                Statement statement = connection.createStatement();
                ResultSet resultSet = statement.executeQuery("select  stock from test.sku_stock where id = 1");
                int stock = 0;
                while (resultSet.next()) {
                    stock = resultSet.getInt(1);
                }
                // 产生随机扣减库存顺序 使得a线程锁住商品1,b线程锁住商品2,会出现
                //com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
                Random random = new Random();
                int tmp = random.nextInt();
                // 若测试死锁则进行 不同顺序的扣减
                if (testDeadLock) {
                    if (tmp % 2 == 0) {
                        buy12(stock, statement, testDeadLock);
                    } else {
                        buy21(stock, statement, testDeadLock);
                    }
                } else {
                    buy12(stock, statement, testDeadLock);
                }
                connection.commit();
                statement.close();
                connection.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void buy12(int stock, Statement statement, boolean testDeadLock) throws Exception {
        if (stock > 0) {
            statement.execute("update test.sku_stock set stock=stock-1 where id=1");
            if (testDeadLock) {
                // 模拟死锁
                Thread.sleep(1000);
                statement.execute("update test.sku_stock set stock=stock-1 where id=2");
            }
        }
    }

    private static void buy21(int stock, Statement statement, boolean testDeadLock) throws Exception {
        if (stock > 0) {
            statement.execute("update test.sku_stock set stock=stock-1 where id=2");
            if (testDeadLock) {
                // 模拟死锁
                Thread.sleep(1000);
                statement.execute("update test.sku_stock set stock=stock-1 where id=1");
            }
        }
    }

}

解决方案一

在执行select时,进行加锁select  stock from test.sku_stock where id = 1 for update 容易出现死锁

同时高并发下容易出现Lock wait timeout exceeded; try restarting transaction的错误

Mysql采用InnoDB模式,默认参数:innodb_lock_wait_timeout设置锁等待的时间是50s,一旦数据库锁超过这个时间就会报错

解决方案二

乐观锁,但是需要修改数据的隔离级别为读已提交,不然可能会导致死循环

解决方案三

update在mysql中本身就是一个行锁或是表锁的过程,将库存校验也加入到update中,于是:update.... where id=xxx and stock >= decutStock(余量大于扣减库存数)

于是代码修改如下

    private static void buy12(int stock, Statement statement, boolean testDeadLock) throws Exception {
        if (stock > 0) {
            statement.execute("update test.sku_stock set stock=stock-1 where id=1 and stock >= 1");
            if (testDeadLock) {
                // 模拟死锁
                Thread.sleep(1000);
                statement.execute("update test.sku_stock set stock=stock-1 where id=2 and stock >= 1");
            }
        }
    }

    private static void buy21(int stock, Statement statement, boolean testDeadLock) throws Exception {
        if (stock > 0) {
            statement.execute("update test.sku_stock set stock=stock-1 where id=2 and stock >= 1");
            if (testDeadLock) {
                // 模拟死锁
                Thread.sleep(1000);
                statement.execute("update test.sku_stock set stock=stock-1 where id=1 and stock >= 1");
            }
        }
    }

但是直接继续数据库操作,在量更大的情况下仍然无法起到很好的效果,太多的update 都需要等待锁,大量的请求超时,于是继续分析更好的方法。(此处需要通过存储一条库存记录来保证可以还库存的操作)

单实例存在的容量和性能上限问题,可以采用分库分表设计,主要通过数据的水平拆分实现不同商品的库存扣减请求路由到不同的数据库。

解决方案四 Redis扣减库存-只能实现最终一致性

通过Redis中存储对应的商品和库存信息,在进行商品库存扣减时直接通过redis+lua进行库存扣减,库存充足则异步刷入数据库。

将当前加减的数量丢给消息队列,由消费端慢慢消化这些操作到数据库。

问题二 死锁

在执行更新操作过程中,如果出现线程1 锁住A商品,请求扣减商品B,而线程2锁住了B商品,请求扣减商品A。此时就出现了死锁情况

Deadlock found when trying to get lock; try restarting transaction

解决方案一:

通过串行化队列去执行扣减商品,性能十分差

解决方案二:

直接破坏循环的条件,如果每个线程都是通过扣减A商品在扣减B商品,那么就能避免死锁。(但还是可能出现和select。。。for update一样的问题Lock wait timeout exceeded; try restarting transaction的错误 这这时候就需要提高处理速度了)

如果提升线程的处理速度,在B线程执行之前就把A执行完可以避免死锁,但是好像不太可行

解决方案三:

通过Redis+lua时,通过生成对应的库存扣减记录,然后通过异步消息队列mq等进行修改库存。实现最终一致性。

问题:通过redis如果redis挂了怎么办?

因为生成了库存扣减记录进行最终数据库中的库存进行扣减等,并对该库存扣减记录进行状态判断,两个状态字段分别为库存扣减状态和库存返回状态字段,如果初始都为0,若库存扣减成功则进行状态修正,若订单取消则对库存返回状态进行修正。此时redis挂了

需要从数据库中重新读取,需要对其库存进行计算。

此时必须保证redis的高可用,进行集群搭建

 

最后

以上就是眯眯眼黄豆为你收集整理的多线程执行数据库MYSQL扣减库存问题分析与解决方案问题一:超卖问题二 死锁问题:通过redis如果redis挂了怎么办?的全部内容,希望文章能够帮你解决多线程执行数据库MYSQL扣减库存问题分析与解决方案问题一:超卖问题二 死锁问题:通过redis如果redis挂了怎么办?所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部