精华内容
下载资源
问答
  • SQL Server并发死锁学习总结
    更多相关内容
  • 并发性:死锁 1 死锁的概念 2 产生死锁的条件和处理 3 死锁的预防 4 死锁的避免 5 死锁的检测与解除 6 死锁的综合处理策略 1 死锁的概念 死锁: 指多个进程因竞争共享资源而造成的一种僵局若无外力作用这些进程都将...
  • 并发死锁问题

    1 什么是死锁

    • 发生在并发
    • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。

     

    如果个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁 


    2 死锁的影响

    死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力

    • 数据库中:检测并放弃事务
    • JVM中:无法自动处理

    3 几率不高但危害大

    • 不一定发生,但是遵守 “墨菲定律
    • 一旦发生,多是高并发场景,影响用户多
    • 整个系统崩溃、子系统崩溃、性能降低
    • 压力测试无法找出所有潜在的死锁

    4 发生死锁的例子

    4.1 最简单的情况

    /**
     * 必定发生死锁的情况
     */
    public class MustDeadLock implements Runnable {
        static final Object LOCK_A = new Object();
        static final Object LOCK_B = new Object();
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"进入了run方法...");
            if ("A".equals(Thread.currentThread().getName())) {
                synchronized (LOCK_A) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK_B) {
                        System.out.println("线程A成功拿到两把锁");
                    }
                }
            }
            if ("B".equals(Thread.currentThread().getName())) {
                synchronized (LOCK_B) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK_A) {
                        System.out.println("线程B成功拿到两把锁");
                    }
                }
            }
        }
        public static void main(String[] args) {
            MustDeadLock run = new MustDeadLock();
            Thread t1 = new Thread(run, "A");
            Thread t2 = new Thread(run, "B");
            t1.start();
            t2.start();
        }
    }
    /**
     * jps -l
     * jstack 进程号
     */
    public class HoldLockThread implements Runnable {
        private final Object lockA;
        private final Object lockB;
        public HoldLockThread(Object lockA, Object lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }
        @Override
        public void run() {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + " 自己持有" + lockA + ",尝试获得" + lockB);
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException ignored) {
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "自己持有" + lockB + ",尝试获得" + lockA);
                }
            }
        }
        public static void main(String[] args) {
            Object lockA = "lockA";
            Object lockB = "lockB";
            new Thread(new HoldLockThread(lockA, lockB), "AAA").start();
            new Thread(new HoldLockThread(lockB, lockA), "BBB").start();
        }
    }

    4.2 实际生产中的例子:转账

    • 需要把锁
    • 获取两把锁成功,且余额大于0,则扣除转出人,增加收款人的余额,是原子操作
    • 顺序相反导致死锁
    /**
     * 转账时候遇到死锁,一旦打开注释,便会发生死锁
     */
    public class TransferMoney {
        //A的账户余额
        static Account a = new Account(500);
        //B的账户余额
        static Account b = new Account(500);
        /**
         * 转账业务
         * @param from   钱从哪里来
         * @param to     钱到哪里去
         * @param amount 转账金额
         */
        public static void transferMoney(Account from, Account to, int amount) {
            //注意这里的加锁对象是谁
            synchronized (from) {
                //try {
                //    Thread.sleep(500);
                //} catch (InterruptedException e) {
                //    e.printStackTrace();
                //}
                synchronized (to) {
                    if (from.balance - amount < 0) {
                        System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
                    }
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
                }
            }
        }
        //账号类
        static class Account {
            //余额
            int balance;
            public Account(int balance) {
                this.balance = balance;
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Runnable run = () -> {
                //A->B
                if ("A".equals(Thread.currentThread().getName())) {
                    transferMoney(a, b, 200);
                }
                //B->A
                if ("B".equals(Thread.currentThread().getName())) {
                    transferMoney(b, a, 300);
                }
            };
            Thread t1 = new Thread(run, "A");
            Thread t2 = new Thread(run, "B");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("a的余额" + a.balance);
            System.out.println("b的余额" + b.balance);
        }
    }

    4.3 模拟多人随机转账

    5万人很多,但是依然会发生死锁,墨菲定律

    复习:发生死锁几率不高危害大

    public class TransferMoney {
        /**
         * 转账业务
         * @param from   钱从哪里来
         * @param to     钱到哪里去
         * @param amount 转账金额
         */
        public static void transferMoney(Account from, Account to, int amount) {
            //注意这里的加锁对象是谁
            synchronized (from) {
                synchronized (to) {
                    if (from.balance - amount < 0) {
                        System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
                    }
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
                }
            }
        }
        //账号类
        static class Account {
            //余额
            int balance;
            public Account(int balance) {
                this.balance = balance;
            }
        }
    }
    /**
     * 多人同时转账,依然很危险
     */
    public class MultiTransferMoney {
        public static void main(String[] args) {
            //账户数量(数量越大发生死锁机率越小)
            int numAccounts = 5000;
            //每个账户的初始金额
            int numMoney = 1000;
            //转账次数(次数越小发生死锁机率越小)
            int numIterations = 1000000;
            //多少个人模拟转账
            int numThreads = 20;
            Random rnd = new Random();
            Account[] accounts = new Account[numAccounts];
            //初始化账户
            for (int i = 0; i < accounts.length; i++) {
                accounts[i] = new Account(numMoney);
            }
            Runnable run = () -> {
                //我转给你,你转给我的概率很小
                for (int i = 0; i < numIterations; i++) {
                    int fromAcct = rnd.nextInt(numAccounts);
                    int toAcct = rnd.nextInt(numAccounts);
                    int amount = rnd.nextInt(numMoney);
                    TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
                }
                System.out.println("运行结束");
            };
            //模拟numThreads个人同时转账
            for (int i = 0; i < numThreads; i++) {
                new Thread(run).start();
            }
            //形成环了,卡死
        }
    }

    5 死锁的4个必要条件

    缺一不可,同时满足

    1. 互斥条件
    2. 请求与保持条件
    3. 不剥夺条件
    4. 循环等待条件

    6 如何定位死锁?

    6.1 jstack

    执行4.2 实际生产中的例子:转账 的代码

    1. 找到java程序对应的进程pid  jvisualvm

    2. 执行jstack命令 jstack 40732

     

    分析 4.3 模拟多人随机转账

     

    6.2 ThreadMXBean

    /**
     * 用ThreadMXBean检测死锁
     */
    public class ThreadMXBeanDetection implements Runnable {
        static final Object LOCK_A = new Object();
        static final Object LOCK_B = new Object();
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "进入了run方法...");
            if ("A".equals(Thread.currentThread().getName())) {
                synchronized (LOCK_A) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK_B) {
                        System.out.println("线程A成功拿到两把锁");
                    }
                }
            }
            if ("B".equals(Thread.currentThread().getName())) {
                synchronized (LOCK_B) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (LOCK_A) {
                        System.out.println("线程B成功拿到两把锁");
                    }
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            ThreadMXBeanDetection run = new ThreadMXBeanDetection();
            Thread t1 = new Thread(run, "A");
            //System.out.println("线程A,id=" + t1.getId());
            Thread t2 = new Thread(run, "B");
            //System.out.println("线程B,id=" + t2.getId());
            t1.start();
            t2.start();
            //让它进入死锁
            Thread.sleep(1000);
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            //发现死锁的线程
            long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                for (int i = 0; i < deadlockedThreads.length; i++) {
                    ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                    System.out.println("发现死锁:" + threadInfo.getThreadName() + " id:" + deadlockedThreads[i]);
                }
            }
        }
    }

     

    7 修复死锁的策略

    线上问题都需要防患于未然,不造成损失地扑灭几乎已经是不可能

    保存案发现场然后立刻重启服务器

    暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版

    7.1 避免策略 

    避免策略:哲学家就餐的换手方案、转账换序方案

    思路:避免相反的获取锁的顺序

    • 转账时避免死锁
    • 实际上不在乎获取锁的顺序
    • 通过hashcode来决定获取锁的顺序、冲突时需要 “加时赛
    • 主键就更方便

    public class TransferMoney {
        //A的账户余额
        static Account a = new Account(500);
        //B的账户余额
        static Account b = new Account(500);
        static final Object LOCK = new Object();
        /**
         * 转账业务
         * @param from   钱从哪里来
         * @param to     钱到哪里去
         * @param amount 转账金额
         */
        public static void transferMoney(Account from, Account to, int amount) {
            class Helper {
                public void transfer() {
                    if (from.balance - amount < 0) {
                        System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
                        return;
                    }
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
                }
            }
            //获取锁的顺序一致
            //AB转账,A的hash值小于B
            //A->B 先获取A的锁,在获取B的锁
            //B->A 先获取A的锁,在获取B的锁
            int fromHash = System.identityHashCode(from);
            int toHash = System.identityHashCode(to);
            if (fromHash < toHash) {
                synchronized (from) {
                    synchronized (to) {
                        new Helper().transfer();
                    }
                }
            } else if (fromHash > toHash) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
            //通过hashcode来决定获取锁的顺序、冲突时需要 “加时赛”
            else {
                synchronized (LOCK) {
                    synchronized (to) {
                        synchronized (from) {
                            new Helper().transfer();
                        }
                    }
                }
            }
        }
        //账号类
        static class Account {
            //余额
            int balance;
    
            public Account(int balance) {
                this.balance = balance;
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                //A->B
                transferMoney(a, b, 200);
            }, "A");
            Thread t2 = new Thread(() -> {
                //B->A
                transferMoney(b, a, 300);
            }, "B");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("a的余额" + a.balance);
            System.out.println("b的余额" + b.balance);
        }
    }

    代码演示:哲学家进入死锁

    多种解决方案

    死锁资源耗尽的风险

    1. 服务员检查(避免策略)
    2. 改变—个哲学家拿叉子的顺序(避免策略)
    3. 餐票(避免策略)
    4. 领导调节(检测与恢复策略)
    /**
     * 演示哲学家就餐问题导致的死锁
     */
    public class DiningPhilosophers {
        //哲学家
        public static class Philosopher implements Runnable {
            //左边筷子
            private Object leftChopstick;
            //右边筷子
            private Object rightChopstick;
            public Philosopher(Object leftChopstick, Object rightChopstick) {
                this.leftChopstick = leftChopstick;
                this.rightChopstick = rightChopstick;
            }
            @Override
            public void run() {
                try {
                    while (true) {
                        //思考
                        doAction("Thinking");
                        synchronized (leftChopstick) {
                            //拿起左边的筷子
                            doAction("Picked up left chopstick");
                            synchronized (rightChopstick) {
                                //拿起右边的筷子
                                doAction("Picked up right chopstick - eating");
                                //放下右边的筷子
                                doAction("Put down right chopstick");
                            }
                            //放下左边的筷子
                            doAction("Put down left chopstick");
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //动作:打印+等待
            private void doAction(String action) throws InterruptedException {
                System.out.println(Thread.currentThread().getName() + " " + action);
                //[0,10)
                Thread.sleep((long) (Math.random() * 10));
            }
        }
        public static void main(String[] args) {
            //哲学家数组
            Philosopher[] philosophers = new Philosopher[5];
            //筷子数组 筷子跟数==哲学家人数
            Object[] chopsticks = new Object[philosophers.length];
            //初始化
            for (int i = 0; i < chopsticks.length; i++) {
                chopsticks[i] = new Object();
            }
            for (int i = 0; i < philosophers.length; i++) {
                //左筷子
                Object leftChopstick = chopsticks[i];
                //右筷子
                Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
                //1.发生死锁 都是Picked up left chopstick
                //philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
                
                //2.改变—个哲学家拿叉子的顺序(避免策略)
                //让最后一个哲学家,反着拿
                if (i == philosophers.length - 1) {
                    philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
                } else {
                    philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
                }
                new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
            }
        }
    }

    7.2 检测与恢复策略 

    检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

    7.3 鸵鸟策略

    鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复


    8 实际工程中如何避免死锁

    • Lock的tryLock(long time, TimeUnit unit)
    • synchronized不具备尝试锁的能力
    • 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
    • 获取锁失败:打日志、发报警邮件、重启

    代码演示:退一步海阔天空

    /**
     * 用tryLock来避免死锁
     */
    public class TryLockDeadlock implements Runnable {
        static Lock lock1 = new ReentrantLock();
        static Lock lock2 = new ReentrantLock();
        @Override
        public void run() {
            //尝试获取次数
            for (int i = 0; i < 100; i++) {
                if ("A".equals(Thread.currentThread().getName())) {
                    try {
                        if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程A获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                System.out.println("线程A获取到了锁2");
                                System.out.println("线程A成功获取到了两把锁");
                                //do something
                                lock2.unlock();
                                lock1.unlock();
                                break;
                            } else {
                                System.out.println("线程A尝试获取锁2失败,已重试");
                                lock1.unlock();
                                Thread.sleep(new Random().nextInt(1000));
                            }
                        } else {
                            System.out.println("线程A获取锁1失败,已重试");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if ("B".equals(Thread.currentThread().getName())) {
                    try {
                        if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程B获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                                System.out.println("线程B获取到了锁1");
                                System.out.println("线程B成功获取到了两把锁");
                                //do something
                                lock1.unlock();
                                lock2.unlock();
                                break;
                            } else {
                                System.out.println("线程B尝试获取锁1失败,已重试");
                                lock2.unlock();
                                Thread.sleep(new Random().nextInt(1000));
                            }
                        } else {
                            System.out.println("线程B获取锁2失败,已重试");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public static void main(String[] args) {
            TryLockDeadlock run = new TryLockDeadlock();
            new Thread(run,"A").start();
            new Thread(run,"B").start();
        }
    }

    9 其他活性故障

    • 死锁是最常见的活跃性问题,不过除了死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题
    • 活锁(LiveLock)
    • 饥饿

    9.1 活锁

    • 死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)
    • 活锁:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉
    • 在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源

    9.1.2 什么是活锁

    • 虽然线程并没有阻塞,也始终在运行(所以叫做 “活” 锁,线程是 “活” 的),但是程序却得不到进展,因为线程始终重复做同样的事
    • 如果下图中描述的是 “死锁”,那么这里的情况就是,两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待
    • 如果下图中描述的是 “活锁”,那么这里的情况就是,双方都不停地对对方说 “你先起来吧,你先起来吧” ,双方都一直在说话,在运行
    • 死锁和活锁的结果是一样的,就是谁都不能先抬头

    9.1.3 代码演示

    /**
     * 演示活锁问题
     */
    public class LiveLock {
        //餐具:勺子(只有一个)
        static class Spoon {
            //勺子拥有者
            private Diner owner;
            public Spoon(Diner owner) {
                this.owner = owner;
            }
            public void setOwner(Diner owner) {
                this.owner = owner;
            }
            //使用勺子
            public synchronized void use() {
                System.out.printf("%s吃完了!", owner.name);
            }
        }
        //就餐者:正在吃饭的那个人
        static class Diner {
            //就餐者名字
            private String name;
            //就餐者是否饥饿
            private boolean isHungry;
            public Diner(String name) {
                this.name = name;
                //初始化饥饿
                isHungry = true;
            }
            /**
             * 就餐方法
             * @param spoon 餐具(物资匮乏,只有一个)
             * @param spouse 陪我一起就餐的人
             */
            public void eatWith(Spoon spoon, Diner spouse) {
                while (isHungry) {
                    //发现勺子拥有者不是自己,伴侣可能正在用
                    if (spoon.owner != this) {
                        try {
                            //等伴侣吃完
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        continue;
                    }
                    //已经拿到勺子了
                    //开始谦让,先询问对方是否饥饿
    
                    //!!!发生活锁,互相谦让,把勺子给对方
                    //if (spouse.isHungry) {
    
                    //原因:重试机制不变,消息队列始终重试,吃饭始终谦让
                    //解决:加入随机因素(以太网的指数退避算法)
                    if (spouse.isHungry && new Random().nextInt(10) < 9) {
                        System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
                        //勺子给对方
                        spoon.setOwner(spouse);
                        continue;
                    }
                    //对方不饿,自己吃
                    spoon.use();
                    //不在饥饿
                    isHungry = false;
                    System.out.println(name + ": 我吃完了");
                    //吃完了,勺子给对方
                    spoon.setOwner(spouse);
                }
            }
        }
        public static void main(String[] args) {
            Diner husband = new Diner("药水哥");
            Diner wife = new Diner("周淑怡");
            //物资匮乏,只有一个勺子
            Spoon spoon = new Spoon(husband);
            new Thread(() -> husband.eatWith(spoon, wife)).start();
            new Thread(() -> wife.eatWith(spoon, husband)).start();
        }
    }

    9.1.4 工程中的活锁实例:消息队列

    • 策略:消息如果处理失败,就放在队列开头重试
    • 由于依赖服务出了问题,处理该消息一直失败
    • 没阻塞,但程序无法继续
    • 解决:放到队列尾部、重试限制

    9.1.5 如何解决活锁问题

    加入随机因素


    9.2 饥饿

    当线程需要某些资源(例如CPU),但是却始终得不到

    线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁

    饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差

    9.3 线程优先级 

    10个级别,默认5


    程序设计不应依赖于优先级

    • 不同操作系统不一样
    • 优先级会被操作系统改变

    10 常见面试问题

    1. 写一个必然死锁的例子,生产中什么场景下会发生死锁?

    • 在一个方法中获取多个锁,容易产生死锁

    2. 发生死锁必须满足哪些条件

    1. 互斥条件
    2. 请求与保持条件
    3. 不剥夺条件
    4. 循环等待条件

    3. 如何定位死锁?

    1. jstack
    2. ThreadMXBean

    4. 有哪些解决死锁问题的策略

    1. 避免策略:哲学家就餐的换手方案、转账换序方案
    2. 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
    3. 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复

    5.讲一讲经典的哲学家就餐问题

    1. 画出示意图
    2. 给出多种解决方案

    1. 服务员检查(避免策略)
    2. 改变一个哲学家拿叉子的顺序(避免策略)
    3. 餐票(避免策略)
    4. 领导调节(检测与恢复策略)

    6. 实际工程中如何避免死锁?

    1. 设置超时时间
    2. 多使用并发类而不是自己设计锁
    3. 尽量降低锁的使用粒度:用不同的锁而不是一个锁
    4. 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
    5. 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
    6. 避免锁的嵌套:MustDeadLock类
    7. 分配资源前先看能不能收回来:银行家算法
    8. 尽量不要几个功能用同一把锁:专锁专用

    7. 什么是活跃性问题?活锁、饥饿和死锁有什么区别?

    展开全文
  • mysql并发插入死锁

    千次阅读 2020-11-18 19:24:16
    mysql并发插入死锁 场景 多个线程同时执行条件插入语句 SQL: INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) SELECT ?, ?, ?, 0, ?, ? FROM DUAL WHERE NOT EXISTS(SELECT ...

    mysql并发插入死锁

    场景

    多个线程同时执行条件插入语句
    SQL:

    INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) SELECT ?, ?, ?, 0, ?, ? FROM DUAL WHERE NOT EXISTS(SELECT resource FROM system_lock WHERE resource = ?)
    

    状态跟踪

    执行命令查看 innodb 状态

    show engine innodb status
    

    部分结果:

    ------------------------
    LATEST DETECTED DEADLOCK
    ------------------------
    2020-11-18 10:57:27 0x7f8cad4f4700
    *** (1) TRANSACTION:
    TRANSACTION 1484317, ACTIVE 0 sec inserting
    mysql tables in use 2, locked 2
    LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
    MySQL thread id 420033, OS thread handle 140242091288320, query id 11981741 123.157.208.7 root executing
    INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) SELECT '2763d6de-9f0a-4c45-8c0b-dbda640224e3', '10.13.64.103:demo2:43', '675fee90-01eb-4511-b64e-c339be116475', 0, '2020-11-18 18:57:25.798050', '2021-11-19 00:46:37.798050' FROM DUAL WHERE NOT EXISTS(SELECT resource FROM system_lock WHERE resource = '2763d6de-9f0a-4c45-8c0b-dbda640224e3')
    
    *** (1) HOLDS THE LOCK(S):
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1484317 lock mode S
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1484317 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (2) TRANSACTION:
    TRANSACTION 1484315, ACTIVE 0 sec inserting
    mysql tables in use 2, locked 2
    LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
    MySQL thread id 420032, OS thread handle 140241595483904, query id 11981739 123.157.208.7 root executing
    INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) SELECT '2763d6de-9f0a-4c45-8c0b-dbda640224e3', '10.13.64.103:demo2:46', 'ce7a3358-453f-489a-a414-c84403ae1506', 0, '2020-11-18 18:54:29.151050', '2021-11-19 00:43:41.151050' FROM DUAL WHERE NOT EXISTS(SELECT resource FROM system_lock WHERE resource = '2763d6de-9f0a-4c45-8c0b-dbda640224e3')
    
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1484315 lock mode S
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1484315 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    *** WE ROLL BACK TRANSACTION (2)
    

    是因为插入语句使用了条件插入吗?换成单纯的插入试一下

    ------------------------
    LATEST DETECTED DEADLOCK
    ------------------------
    2020-11-18 11:52:20 0x7f8cad4f4700
    *** (1) TRANSACTION:
    TRANSACTION 1498717, ACTIVE 0 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 8 lock struct(s), heap size 1136, 3 row lock(s)
    MySQL thread id 420353, OS thread handle 140241587422976, query id 12160669 123.157.208.18 root update
    INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) VALUES ('5d57d8cb-38cc-49b3-aa93-2c116e076eed', '10.13.64.103:demo2:41', 'a3142732-3e7e-4d0a-8740-700b0dc5af36', 0, now(), '2021-11-19 01:39:31.065050')
    
    *** (1) HOLDS THE LOCK(S):
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1498717 lock mode S
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1498717 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (2) TRANSACTION:
    TRANSACTION 1498721, ACTIVE 0 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s)
    MySQL thread id 420355, OS thread handle 140241588307712, query id 12160673 123.157.208.6 root update
    INSERT INTO system_lock (resource, owner, token, version, lock_time, release_time) VALUES ('5d57d8cb-38cc-49b3-aa93-2c116e076eed', '10.13.64.103:demo2:43', '0bafcc68-c4e2-491f-bf3e-abb7935783f0', 0, now(), '2021-11-19 01:39:31.778050')
    
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1498721 lock mode S
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    
    *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 544 page no 4 n bits 72 index PRIMARY of table `shoulder_platform`.`system_lock` trx id 1498721 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
     0: len 8; hex 73757072656d756d; asc supremum;;
    
    *** WE ROLL BACK TRANSACTION (2)
    

    原理分析

    两个事务都持有该行的 S 锁,期望获取 X 锁时被对方阻塞了。

    锁消除

    删除改行记录(释放锁之后),其中一个事务将成功获取锁,从而执行成功,其他事务因死锁检测而回滚。

    扩展:mysql官方声明-不同语句产生的锁

    SELECT … FROMis a consistent read,无锁
    SELECT … FOR UPDATEandSELECT … FOR SHAREstatements:对于唯一索引,只对需要锁定的记录上record lock,如果不是唯一索引就会加Next-key lock,同时会对不满足条件的第一个record的前面加上gap lock
    update和delete都2一样的策略
    insert比较不一样,需要重点说下
    首先,insert会对要插入的gap加insert intention gap lock,insert intention gap lock不像gap lock一样防止insert,insert intention gap lock互相之间可以共存,这样允许多个insert并发插入不同的位置

    其次,对于要插入的位置会加入排他锁

    多个insert并发(至少3个),当插入的位置一样的时候,比如3个session都insert table id=x,其中第一个获得排他锁,其他两个session会产生duplicate-key error,当duplicate-key error发生时,两个session都会将锁变化为共享锁,下一步获取排他锁,然后第一个session rollback了,两个session互相持有共享锁,无法获得排他锁,导致死锁

    s1 insert id =x 或者 s1 delete id = x

    s2 insert id = x

    s3 insert id = x

    s1 rollback

    1. replace和insert一样

    2. INSERT … ON DUPLICATE KEY UPDATE与insert不一样,当遇到 duplicate-key时,对于primary key获取排他记录锁,对于unique index获取Next-key lock,不会死锁

    建议

    非热点(如抢购)情况,使用悲观锁,先保证业务的正确性和数据的一致性,且能够保证较好的代码可读性,减少维护成本。

    若无过多业务逻辑,则直接使用数据库行锁,如

    select xxx from tt where field = f for update
    执行更新 sql
    

    若有较耗时的业务逻辑,则使用业务锁,如redis锁、zookeeper 锁


    优化性能和死锁问题的思路:

    尽量将热点行的操作延后
    控制并发度,加队列

    官方声明-不同语句产生的锁

    展开全文
  • 死锁产生的原因和解锁的方法  产生死锁的四个必要条件:  (1) 互斥条件:一个资源每次只能被一个进程使用。  (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。  (3) 不...
  • 发生在并发中 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁 2.发生死锁的例子 2.1简单的例子 当类的对象...

    目录

    ​​​​​​​1.什么是死锁

    2.发生死锁的例子

    2.1简单的例子

    2.2生产中的例子-转账

    2.3模拟多人转账

    3.死锁的4个必要条件

    4.如何定位死锁

    5.   如何避免死锁

    5.1破坏占用且等待条件,一次性申请全部资源

    5.2破坏循环等待条件

    6.实际工作中如何避免死锁

    1.什么是死锁

    • 发生在并发中
    • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁

    2.发生死锁的例子

    2.1简单的例子

    • 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2
    • 而T1在睡眠的时候另一个flag=0的对象(T2)启动,先锁定O2,睡眠500毫秒,等待T1释放O1
    • T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定
    • T2睡眠结束后需要锁定 O1才能继续执行,而此时O1已被T1锁定
    • T1和T2互相等待,都需要对方多订的资源才能继续执行,从而产生死锁
    public class MustDeadLock implements Runnable {
    
        int flag = 1;
        static Object o1 = new Object();
        static Object o2 = new Object();
    
        public static void main(String[] args) {
            MustDeadLock r1 = new MustDeadLock();
            MustDeadLock r2 = new MustDeadLock();
            r1.flag = 1;
            r2.flag = 0;
            new Thread(r1).start();
            new Thread(r2).start();
        }
    
        @Override
        public void run() {
            if (flag == 1){
                synchronized (o1){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2){
                        System.out.println("r1成功拿到两把锁");
                    }
                }
            }
    
            if (flag == 0){
                synchronized (o2){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1){
                        System.out.println("r2成功拿到两把锁");
                    }
                }
            }
        }
    }
    

    2.2生产中的例子-转账

    • 需要两把锁
    • 获取两把锁成功,且余额大于0,则扣除转出人,增加收款人的余额,是原子操作
    • 顺序相反导致死锁
    public class TransferMoney implements Runnable{
    
        int flag = 1;
    
        static Account a = new Account(500);
        static Account b = new Account(500);
    
        public static void main(String[] args) throws InterruptedException {
            TransferMoney r1 = new TransferMoney();
            TransferMoney r2 = new TransferMoney();
            r1.flag = 1;
            r2.flag = 0;
            Thread t1 = new Thread(r1);
            Thread t2 = new Thread(r2);
            t1.start();
            t2.start();
            t1.join();
            t1.join();
            System.out.println("a的余额"+a.balance);
            System.out.println("b的余额"+b.balance);
        }
    
        @Override
        public void run() {
            if (flag ==1){
                transferMoney(a,b,200);
            }
            if (flag ==0){
                transferMoney(b,a,200);
            }
        }
    
        public static void transferMoney(Account from, Account to, int amount) {
    
            synchronized (from){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (to){
                    if (from.balance - amount <0){
                        System.out.println("余额不足,转账失败");
                    }
                    from.balance-=amount;
                    to.balance+=amount;
                    System.out.println("转账成功");
                }
            }
        }
    
        static class Account{
            int balance;
    
            public Account(int balance) {
                this.balance = balance;
            }
        }
    
    }
    

    2.3模拟多人转账

    public class MultTransferMoney {
        public static final int NUM_ACCOUNTS = 500;
        public static final int NUM_MONEY = 1000;
        public static final int NUM_ITERATIONS = 1000;
        public static final int NUM_THREADS = 20;
    
        public static void main(String[] args) {
            final Random rnd = new Random();
            final Account[] accounts = new Account[NUM_ACCOUNTS];
            for (int i = 0; i < accounts.length; i++) {
                accounts[i] = new Account(NUM_MONEY);
            }
            class TransferThread extends Thread{
                @Override
                public void run(){
                    for (int i = 0; i < NUM_ITERATIONS; i++) {
                        int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                        int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                        int amount = rnd.nextInt(NUM_MONEY);
                        TransferMoney.transferMoney(accounts[fromAcct],accounts[toAcct],amount);
                    }
                }
            }
            for (int i = 0; i < NUM_THREADS; i++) {
                new TransferThread().start();
            }
        }
    }
    

    3.死锁的4个必要条件

    • 互斥,共享资源X和Y只能被一个线程占用;
    • 占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
    • 不可抢占,其他线程不能强行抢占线程T1占有的资源;
    • 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。

    4.如何定位死锁

    使用ThreadMaxBean

    public class ThreadMXBeanD implements Runnable{
        int flag = 1;
        static Object o1 = new Object();
        static Object o2 = new Object();
    
        public static void main(String[] args) {
            ThreadMXBeanD r1 = new ThreadMXBeanD();
            ThreadMXBeanD r2 = new ThreadMXBeanD();
            r1.flag = 1;
            r2.flag = 0;
            new Thread(r1).start();
            new Thread(r2).start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ThreadMXBean threadMXBean =  ManagementFactory.getThreadMXBean();
            final long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
            if (deadlockedThreads != null && deadlockedThreads.length > 0){
                for (int i = 0; i < deadlockedThreads.length; i++) {
                    final ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                    // 发生死锁Thread-1
                    // 发生死锁Thread-0
                    System.out.println("发生死锁"+threadInfo.getThreadName());
                }
            }
    
        }
    
        @Override
        public void run() {
            if (flag == 1){
                synchronized (o1){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2){
                        System.out.println("r1成功拿到两把锁");
                    }
                }
            }
    
            if (flag == 0){
                synchronized (o2){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1){
                        System.out.println("r2成功拿到两把锁");
                    }
                }
            }
        }
    }
    

    5.   如何避免死锁

    5.1破坏占用且等待条件,一次性申请全部资源

    class Allocator {
      private List<Object> als = new ArrayList<>();
      // 一次性申请所有资源
      synchronized boolean apply(
        Object from, Object to){
        if(als.contains(from) ||
             als.contains(to)){
          return false;  
        } else {
          als.add(from);
          als.add(to);  
        }
        return true;
      }
      // 归还资源
      synchronized void free(
        Object from, Object to){
        als.remove(from);
        als.remove(to);
      }
    }
    class Account {
      // actr应该为单例
      private Allocator actr;
      private int balance;
      // 转账
      void transfer(Account target, int amt){
        // 一次性申请转出账户和转入账户,直到成功
        while(!actr.apply(this, target))
          ;
        try{
          // 锁定转出账户
          synchronized(this){
    
            // 锁定转入账户
            synchronized(target){
    
              if (this.balance > amt){
                this.balance -= amt;
                target.balance += amt;
              }
            }
          }
        } finally {
          actr.free(this, target)
        }
      } 
    }

    5.2破坏循环等待条件

    class Account {
      private int id;
      private int balance;
      // 转账
      void transfer(Account target, int amt){
        Account left = this        
        Account right = target;    
        if (this.id > target.id) { 
          left = target;           
          right = this;            
        }                          
        // 锁定序号小的账户
        synchronized(left){
          // 锁定序号大的账户
          synchronized(right){ 
            if (this.balance > amt){
              this.balance -= amt;
              target.balance += amt;
            }
          }
        }
      } 
    }

    6.实际工作中如何避免死锁

    1. 设置超时时间
    2. 多食用并发类而不是自己设计类
    3. 尽量降低所得使用粒度,用不同的锁而不是同一个锁
    4. 如果能使用同步代码块,就不使用同步方法,自己指定锁对象
    5. 给你的线程起个有意义的名字,debug和排查的时事半功倍,框架和JDK都遵守这个最佳实践
    6. 避免锁的嵌套:MustDeadLock
    7. 分配资源前先看能不能收回来
    8. 尽量不要几个功能用同一把锁:专锁专用
    展开全文
  • 解决并发时的死锁问题

    万次阅读 2021-12-31 23:27:31
    解决并发时的死锁问题前言向现实世界要答案没有免费的午餐如何预防死锁1. 破坏占用且等待条件2. 破坏不可抢占条件3. 破坏循环等待条件总结 前言 用 Account.class 作为互斥锁,解决银行业务里面的转账问题,虽然这...
  • mysql for update 高并发 死锁研究 mysql for update语句 https://www.cnblogs.com/jtlgb/p/8359266.html For update带来的思考 http://www.cnblog...
  • 今天业务在高并发下出现了死锁问题,sqlsever sql重现,开启两个窗口执行.其中一个会在第二次select时报死锁. Begin Transaction; SELECT * from t_export_inventory_head WHERE agent_code = '1122334455' and cop_...
  • try restarting transaction 3、原因:a、该方法在同一个事物内,高并发情况下,线程1执行到userSealMapper.insert(userSeal)这一行,事物还没有提交;线程2执行到updateUserSealNoDefault(user.getId());处,会将...
  • 对于java流操作,并发处理很大几率导致死锁,应注意流的输出。 比如: 类似这两种就极易导致接口默默超时,锁死。接口应注意类似操作,及时加锁,线程等待
  • 并发访问ORACLE数据库的数据死锁分析和解决措施.pdf
  • 问题: 我这里的问题是多线程情况下没有及时关闭数据库又重新打开了数据库的连接,解决方式是在获取数据库连接的时候采用单例模式。 一、SQLite为什么会出现这种问题? 首先要搞清楚sqlite3自身的机制: ...
  • 欢迎关注!获取更高质量的文章!1. mysql都有什么锁MySQL有三种锁的级别:页级、表级、行级。表级锁:开销小,加锁快;不会出现死锁;...锁定粒度界于表锁和行锁之间,并发度一般算法:next KeyLocks锁,同...
  • 这个时候可以看到会话2,3形成了死锁 会话4查看死锁信息: show engine innodb status; image.png 1.3死锁分析 1.3.1 图1分析 会话2,3被会话1阻塞的时候,会话2,3都显示lock mode S waiting。这个意思是等待共享...
  • 这个 git 存储库包含一个测试来重现与 ACT_RU_AUTHORIZATION 相关的死锁。 该项目包含以下文件: src/ ├── main │ ├── java │ └── resources └── test ├── java │ └── org │ └──...
  • SQL Server死锁使我们经常遇到的问题,数据库操作的死锁是不可避免的,本文并不打算讨论死锁如何产生,重点在于解决死锁。希望对您学习SQL Server死锁方面能有所帮助。
  • 然而,并发执行也带来了新的问题——死锁死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。 下面我们通过...
  • 死锁距离我不遥远,终于还是在高并发时被我碰到了。 DeadLock Found! 尽管编程风格中会尽量避免死锁,但是还是被我碰上了。文章可能看不出来我在做什么事情,只是记录自己的一个排除死锁的过程。 事情起源于开卷...
  • https://www.aneasystone.com/archives/2018/04/solving-dead-locks-four.html
  • 3. 在并发为10的情况下就会触发数据库锁等待和死锁的情况 问题分析 在问题分析之前引入几个概念 事务隔离级别 参考文章《事务的ACID特性》 共享锁(S锁) SELECT 语句时对查询行加的锁类型为共享锁。 共享锁的特性为...
  • mysql并发死锁优化

    千次阅读 2018-05-15 21:01:00
    3.解决死锁:在命令行输入show engine innodb statusG可以显示最近死锁的信息,然后加以解决其次可以设置锁等待时间,这个参数innodb_lock_wait_timeout增加锁等待时间,即增大下面配置项参数值,单位为秒(s...
  • Mysql 并发引起的死锁问题 平台的某个数据库上面有近千个连接,每个连接对应一个爬虫,爬虫将爬来的数据放到cdb里供后期分析查询使用。前段时间经常出现cdb查询缓慢,cpu占有率高的现象。通过show processlist后...
  • Mysql有很多坑,对Mysql多线程支持这块不是很熟的话就会莫名其妙地发生...多线程线程并发操作时最容易产生死锁问题。所以很多大数据的操作一般都采用NoSQL数据库方案来处理,或者读写分离,只需要做好幂等设计即可。
  • Mysql并发时经典常见的死锁原因及解决方法 1.mysql都有什么锁 MySQL有三种锁的级别:页级、表级、行级。 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 行级锁:开销...
  • 一、处理解决死锁的思路 处理思路: 预防 → 避免 → 检测 → 解除。 如果能预防是最好, 其次是尽可能的避免,最后如果检测到死锁, 那就想办法尽快的解除它。 二、产生死锁的四个要素 互斥条件:一个资源每次只能...
  • 操作的是同一张表,先查询是否已经有,没有的话进行新增 重现步骤 当并发的时候出现此类错误 报错信息 Error updating database. Cause: ...
  • 并发死锁与饥饿

    2019-07-24 19:27:11
    并发处理中通常需要解决两个问题:死锁与饥饿 死锁的原理 可以把死锁定义为一组相互竞争系统资源或进行通信的进程之间的“永久性”阻塞。当一组进程中的每个进程都在等待某个事件,而只有在这组进程中的其他被阻塞...
  • 【开卷有益】记录一次高并发下的死锁解决思考过程开卷有益,好久没写博客了,最近工作也挺忙的。死锁距离我不遥远,终于还是在高并发时被我碰到了。DeadLock Found!尽管编程风格中会尽量避免死锁,但是还是被我碰上...
  • Mysql并发插入引发的死锁

    千次阅读 2019-07-16 11:30:17
    项目中有一张业务关联表t_biz_ref,最近给该表建立了复合索引,测试阶段并发执行插入操作频繁出现死锁情况。 t_biz_ref表结构 CREATE TABLE `t_biz_ref` ( `id` varchar(32) NOT NULL COMMENT 'id', `app_key` v...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 169,409
精华内容 67,763
关键字:

并发死锁

友情链接: ADODataBase3.rar