• 如何处理高并发情况下的DB插入


    插入数据库,在大家开发过程中是很经常的事情,假设我们有这么一个需求:

    1、  我们需要接收一个外部的订单,而这个订单号是不允许重复的

    2、  数据库对外部订单号没有做唯一性约束

    3、  外部经常插入相同的订单,对于已经存在的订单则拒绝处理

    对于这个需求,很简单我们会用下面的代码进行处理(思路:先查找数据库,如果数据库存在则直接退出,否则插入)

    package com.yhj.test;

     

    import com.yhj.dao.OrderDao;

    import com.yhj.pojo.Order;

     

    /**

     * @Description:并发测试用例

     * @Author YHJ  create at 2011-7-7 上午08:41:44

     * @FileName com.yhj.test.TestCase.java

     */

    public class TestCase {

        /**

         * data access object class for deal order

         */

        private OrderDao orderDao;

     

        /**

         * @Description:插入测试

         * @param object 要插入的object实例

         * @author YHJ create at 2011-7-7 上午08:43:15

         * @throws Exception

         */

        public void doTestForInsert(Order order) throws Exception {

           Order orderInDB = orderDao.findByName(order.getOrderNo());

           if(null != orderInDB)

               throw new Exception("the order has been exist!");

           orderDao.save(order);

        }

       

    }

    这样很显然,在单线程下是没问题的,但是多线程情况下就会出现一个问题,线程1先去访问DB,查找没有,开始插入,这时候线程2又来查找DB,而此时线程1插入的事务还没有提交,线程2没有查到该数据,也进行插入,于是,问题出现了,插入了2条一样订单。

    对于这种情况,好像如果不用数据库做唯一性约束又不借助外部其他的一些工具,是没有办法实现的。那怎么做呢?

    引入缓存,我们看下面的代码

    package com.yhj.test;

     

    import com.yhj.dao.OrderDao;

    import com.yhj.pojo.Order;

    import com.yhj.util.MemcacheUtil;

    import com.yhj.util.MemcacheUtil.UNIT;

     

    /**

     * @Description:并发测试用例

     * @Author YHJ  create at 2011-7-7 上午08:41:44

     * @FileName com.yhj.test.TestCase.java

     */

    public class TestCase {

        /**

         * data access object class for deal order

         */

        private OrderDao orderDao;

     

        /**

         * @Description:插入测试

         * @param object 要插入的object实例

         * @author YHJ create at 2011-7-7 上午08:43:15

         * @throws Exception

         */

        public void doTestForInsert(Order order){

           String key=null;

           try{

               Order orderInDB = orderDao.findByName(order.getOrderNo());

               //查DB,如果数据库已经有则抛出异常

               if(null != orderInDB)

                  throw new Exception("the order has been exist!");

               key=order.getOrderNo();

               //插缓存,原子性操作,插入失败 表明已经存在

               if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

                  throw new Exception("the order has been exist!");

               //插DB

               orderDao.save(order);

           }catch (Exception e) {

               e.printStackTrace();

           }finally{

               MemcacheUtil.del(key);

           }

        }

     

    }

    运行步骤如下:

    1、  查找数据库,如果数据库已经存在则抛出异常

    2、  插入缓存,如果插入失败则表明缓存中已经存在,抛出异常

    3、  如果上述2步都没有抛出异常,则执行插入数据库的操作

    4、  删除缓存

    在并发的情况下,线程1先查找数据库,发现没有,继续执行,写缓存,这时候线程2开始查找数据库,发现没有,则写缓存,结果缓存中已经存在,写缓存失败,抛出异常,返回已存在。线程1执行插入数据库成功,删除缓存。以后再来的线程发现数据库已经存在了,则不在向下执行,直接返回.

    机器异常情况下,不能执行finally语句,但是放在memcache中的数据会在1分钟后超时。

    貌似没有问题。使用LodeRunner测试100个并发的操作,发现仍然有重复的订单插入,这个是为什么呢?我们再来看这段代码!

    public void doTestForInsert(Order order){

           String key=null;

           try{

               Order orderInDB = orderDao.findByName(order.getOrderNo());

               //查DB,如果数据库已经有则抛出异常

               if(null != orderInDB)

                  throw new Exception("the order has been exist!");

               key=order.getOrderNo();

               //插缓存,原子性操作,插入失败 表明已经存在

               if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

                  throw new Exception("the order has been exist!");

               //插DB

               orderDao.save(order);

           }catch (Exception e) {

               e.printStackTrace();

           }finally{

               MemcacheUtil.del(key);

           }

        }

    我们所预料的是2个线程同时操作,假设有更多的并发线程呢?

    时刻1

    线程1到达,查数据库,发现没有

    时刻2

    线程1写缓存

    线程2到达,查数据库发现没有

    时刻3

    线程1缓存写入成功,开始写数据库

    线程2开始写缓存

    线程3到达,查数据库,发现没有

    时刻4

    线程1继续插入数据库

    线程2写缓存失败,抛出异常,执行finally

    线程3开始写缓存

    时刻5

    线程1插入数据库成功,开始构建返回结果

    线程2执行finally,删除缓存,开始构建返回结果

    线程3发现缓存不存在(被线程2删除),写缓存

    时刻6

    线程1成功返回

    线程2成功返回

    线程3写缓存成功,开始写数据库

    时刻7

    线程3写数据库成功,返回

    因此上述代码仍然有插入多条重复记录的可能,我们在并发20的测试中发现成功插入了5笔订单,其中4笔是不应该插入的!

    那我们应该怎么解决呢?其实只要解决一个问题,只有插入DB时候的异常是可以删除的,其他地方不应该删除,那能不能将代码改成下面的呢?

        public void doTestForInsert(Order order){

           String key=null;

           try{

               Order orderInDB = orderDao.findByName(order.getOrderNo());

               //查DB,如果数据库已经有则抛出异常

               if(null != orderInDB)

                  throw new Exception("the order has been exist!");

               key=order.getOrderNo();

               //插缓存,原子性操作,插入失败 表明已经存在

               if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

                  throw new Exception("the order has been exist!");

               //插DB

               orderDao.save(order);

               MemcacheUtil.del(key);

           }catch (Exception e) {

               e.printStackTrace();

           }//finally{

    //         MemcacheUtil.del(key);

    //     }

        }

    这样显然不行,为什么呢?

    这样是保证了只有插入DB成功了才会删除缓存,但是当插入DB的时候发生了一个异常,删除缓存就不会再执行,虽然我们有一分钟超时,但意味着我们一分钟内该笔订单是不能再被处理的,而实际上这边订单并没有处理成功,所以这样是不满足需求的!

    继续改进

    代码如下:加一个标志位

    public void doTestForInsert(Order order){

           String key=null;

           boolean needDel=false;

           try{

               Order orderInDB = orderDao.findByName(order.getOrderNo());

               //查DB,如果数据库已经有则抛出异常

               if(null != orderInDB)

                  throw new Exception("the order has been exist!");

               key=order.getOrderNo();

               //插缓存,原子性操作,插入失败 表明已经存在

               if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

                  throw new Exception("the order has been exist!");

               needDel=true;

               //插DB

               orderDao.save(order);

           }catch (Exception e) {

               e.printStackTrace();

           }finally{

               if(needDel)

                  MemcacheUtil.del(key);

           }

        }

    这样是不是完美解决了呢?

    在其他异常执行的时候是不会删除缓存的,我们套在之前的代码上,线程2判断缓存中存在抛出异常执行finally的时候是不会删除缓存的,因此线程3没有机会执行写缓存的操作,从而保证了线程1是唯一能够插入DB的。

    还有没有其他漏洞呢?期待大家发现……

  • 相关阅读:
    TechRoad_oneStep_0509
    TechRoad_0417
    [Network] 判断设备是否能访问 Internet
    云原生相关知识点
    Algorithm: 多项式乘法 Polynomial Multiplication: 快速傅里叶变换 FFT / 快速数论变换 NTT
    Mac下配置Apache服务器
    解决antdv 中input每输入一个字符就失去焦点
    *装饰者模式(Decorator)
    *单例模式(singleton)
    ubuntu20.04 线缆已拔出
  • 原文地址:https://www.cnblogs.com/PerOpt/p/3749021.html
Copyright © 2020-2023  润新知