如何处理高并发情况下的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的。

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

时间: 2024-08-07 04:10:39

如何处理高并发情况下的DB插入的相关文章

Jackson高并发情况下,产生阻塞

情况:在高并发情况下,查看线程栈信息,有大量的线程BLOCKED. 从线程栈得知,线程栈中出现了阻塞,锁在了com.fasterxml.jackson.databind.ser.SerializerCache.untypedValueSerializer(SerializerCache.java:74)上. 1 "catalina-exec-1453" #1525 daemon prio=5 os_prio=0 tid=0x00007f1010098800 nid=0x2675 wai

WCF服务在高并发情况下报目标积极拒绝的异常处理 z

http://www.cnblogs.com/kklldog/p/5037006.html wcf的监控服务,偶尔监控到目标服务会报一个目标积极拒绝的错误.一开始以为服务停止了,上服务器检查目标服务好好的活着.于是开始查原因. 一般来说目标积极拒绝(TCP 10061)的异常主要是2种可能: 1:服务器关机或者服务关闭 2:Client调用的端口错误或者服务器防火墙没开相应的端口 但是我们的服务本身是可以调用的,只是偶尔报这个错误,说明并不是这2个问题造成的.继续google,在stackove

关于WCF服务在高并发情况下报目标积极拒绝的异常处理

最近弄了个wcf的监控服务,偶尔监控到目标服务会报一个目标积极拒绝的错误.一开始以为服务停止了,上服务器检查目标服务好好的活着.于是开始查原因. 一般来说目标积极拒绝(TCP 10061)的异常主要是2种可能: 1:服务器关机或者服务关闭 2:Client调用的端口错误或者服务器防火墙没开相应的端口 但是我们的服务本身是可以调用的,只是偶尔报这个错误,说明并不是这2个问题造成的.继续google,在stackoverflow上看到这样一篇:传送门 1 2 3 4 5 6 7 8 9 10 11

数据库高并发情况下重复值写入的避免 字段组合约束+ SQL SERVER 的SQL语句优化方式小结(转)

10线程同时操作,频繁出现插入同样数据的问题.虽然在插入数据的时候使用了: insert inti tablename(fields....) select @t1,@t2,@t3 from tablename where not exists (select id from tablename where [email protected],[email protected],[email protected]) 当时还是在高并发的情况下无效.此语句也包含在存储过程中.(之前也尝试线判断有无记

高并发情况下Linux系统及kernel参数优化

众所周知在默认参数情况下Linux对高并发支持并不好,主要受限于单进程最大打开文件数限制.内核TCP参数方面和IO事件分配机制等.下面就从几方面来调整使Linux系统能够支持高并发环境. Iptables相关 如非必须,关掉或卸载iptables防火墙,并阻止kernel加载iptables模块.这些模块会影响并发性能. 单进程最大打开文件数限制 一般的发行版,限制单进程最大可以打开1024个文件,这是远远不能满足高并发需求的,调整过程如下: 在#号提示符下敲入: # ulimit–n 6553

高并发情况下Redis 的可用性测试与分析及部署架构说明

一.Redis AOF模式设置 修改配置文件redis.conf参数: appendonly yes # appendfsync always appendfsync everysec # appendfsync no 二.测试方法 创建多线程,其中每一个线程执行一个无限循环向Redis 发送set key-value命令,由于处理器执行一次循环操作的速度非常快,因此这样每一个线程都模拟了一个多并发的情况. <span style="font-size:18px;">cla

Mysql在高并发情况下,防止库存超卖而小于0的解决方案

背景: 本人上次做申领campaign的PHP后台时,因为项目上线后某些时段同时申领的人过多,导致一些专柜的存货为负数(<0),还好并发量不是特别大,只存在于小部分专柜而且一般都是-1的状况,没有造成特别特别严重的后果,但还是要反思了自己的过错. 这次又有新的申领campaign,我翻看了上次的代码逻辑: 正文: [先select后update] beginTranse(开启事务) try{     $result = $dbca->query('select amount from s_st

【转】高并发情况下的单例模式

如果在高并发时候,使用这种单例模式 publci class Singleton{      private static Singleton instance = null;      private Singleton(){} public static Singleton getInstance(){             if(instance == null){                    instance = new Singleton();             } 

J2ee高并发情况下监听器

引言:在高并发下限制最大并发次数,在web.xml中用过滤器设置參数(最大并发数),并设置其它相关參数.具体见代码. 第一步:配置web.xml配置,不懂的地方解释一下:參数50通过參数名maxConcurrent用在filter的实现类中获取,filter-class就是写的实现类, url-pattern就是限制并发时间的url.结束! <filter> <filter-name>ConcurrentCountFilter</filter-name> <fil