高并发下的下单功能设计

功能需求:设计一个秒杀系统

初始方案

商品表设计:热销商品提供给用户秒杀,有初始库存。

@Entity

public class SecKillGoods implements Serializable{

@Id

private String id;

/**

* 剩余库存

*/

private Integer remainNum;

/**

* 秒杀商品名称

*/

private String goodsName;

}

秒杀订单表设计:记录秒杀成功的订单情况

@Entity

public class SecKillOrder implements Serializable {

@Id

@GenericGenerator(name = "PKUUID", strategy = "uuid2")

@GeneratedValue(generator = "PKUUID")

@Column(length = 36)

private String id;

//用户名称

private String consumer;

//秒杀产品编号

private String goodsId;

//购买数量

private Integer num;

}

Dao设计:主要就是一个减少库存方法,其他CRUD使用JPA自带的方法

public interface SecKillGoodsDao extends JpaRepository{

@Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1")

@Modifying(clearAutomatically = true)

@Transactional

int reduceStock(String id,Integer remainNum);

}

数据初始化以及提供保存订单的操作:

@Service

public class SecKillService {

@Autowired

SecKillGoodsDao secKillGoodsDao;

@Autowired

SecKillOrderDao secKillOrderDao;

/**

* 程序启动时:

* 初始化秒杀商品,清空订单数据

*/

@PostConstruct

public void initSecKillEntity(){

secKillGoodsDao.deleteAll();

secKillOrderDao.deleteAll();

SecKillGoods secKillGoods = new SecKillGoods();

secKillGoods.setId("123456");

secKillGoods.setGoodsName("秒杀产品");

secKillGoods.setRemainNum(10);

secKillGoodsDao.save(secKillGoods);

}

/**

* 购买成功,保存订单

* @param consumer

* @param goodsId

* @param num

*/

public void generateOrder(String consumer, String goodsId, Integer num) {

secKillOrderDao.save(new SecKillOrder(consumer,goodsId,num));

}

}

下面就是controller层的设计

@Controller

public class SecKillController {

@Autowired

SecKillGoodsDao secKillGoodsDao;

@Autowired

SecKillService secKillService;

/**

* 普通写法

* @param consumer

* @param goodsId

* @return

*/

@RequestMapping("/seckill.html")

@ResponseBody

public String SecKill(String consumer,String goodsId,Integer num) throws InterruptedException {

//查找出用户要买的商品

SecKillGoods goods = secKillGoodsDao.findOne(goodsId);

//如果有这么多库存

if(goods.getRemainNum()>=num){

//模拟网络延时

Thread.sleep(1000);

//先减去库存

secKillGoodsDao.reduceStock(num);

//保存订单

secKillService.generateOrder(consumer,goodsId,num);

return "购买成功";

}

return "购买失败,库存不足";

}

}

上面是全部的基础准备,下面使用一个单元测试方法,模拟高并发下,很多人来购买同一个热门商品的情况。

@Controller

public class SecKillSimulationOpController {

final String takeOrderUrl = "http://127.0.0.1:8080/seckill.html";

/**

* 模拟并发下单

*/

@RequestMapping("/simulationCocurrentTakeOrder")

@ResponseBody

public String simulationCocurrentTakeOrder() {

//httpClient工厂

final SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();

//开50个线程模拟并发秒杀下单

for (int i = 0; i < 50; i++) {

//购买人姓名

final String consumerName = "consumer" + i;

new Thread(new Runnable() {

@Override

public void run() {

ClientHttpRequest request = null;

try {

URI uri = new URI(takeOrderUrl + "?consumer=consumer" + consumerName + "&goodsId=123456&num=1");

request = httpRequestFactory.createRequest(uri, HttpMethod.POST);

InputStream body = request.execute().getBody();

BufferedReader br = new BufferedReader(new InputStreamReader(body));

String line = "";

String result = "";

while ((line = br.readLine()) != null) {

result += line;//获得页面内容或返回内容

}

System.out.println(consumerName+":"+result);

} catch (Exception e) {

e.printStackTrace();

}

}

}).start();

}

return "simulationCocurrentTakeOrder";

}

}

访问localhost:8080/simulationCocurrentTakeOrder,就可以测试了

预期情况:因为我们只对秒杀商品(123456)初始化了10件,理想情况当然是库存减少到0,订单表也只有10条记录。

实际情况:订单表记录

高并发下的下单功能设计_第1张图片

商品表记录

高并发下的下单功能设计_第2张图片

下面分析一下为啥会出现超库存的情况:

因为多个请求访问,仅仅是使用dao查询了一次数据库有没有库存,但是比较恶劣的情况是很多人都查到了有库存,这个时候因为程序处理的延迟,没有及时的减少库存,那就出现了脏读。如何在设计上避免呢?最笨的方法是对SecKillController的seckill方法做同步,每次只有一个人能下单。但是太影响性能了,下单变成了同步操作。

@RequestMapping("/seckill.html")

@ResponseBody

public synchronized String SecKill

改进方案

根据多线程编程的规范,提倡对共享资源加锁,在最有可能出现并发争抢的情况下加同步块的思想。应该同一时刻只有一个线程去减少库存。但是这里给出一个最好的方案,就是利用Oracle,MySQL的行级锁–同一时间只有一个线程能够操作同一行记录,对SecKillGoodsDao进行改造:

public interface SecKillGoodsDao extends JpaRepository{

@Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1 and g.remainNum>0")

@Modifying(clearAutomatically = true)

@Transactional

int reduceStock(String id,Integer remainNum);

}

仅仅是加了一个and,却造成了很大的改变,返回int值代表的是影响的行数,对应到controller做出相应的判断。

@RequestMapping("/seckill.html")

@ResponseBody

public String SecKill(String consumer,String goodsId,Integer num) throws InterruptedException {

//查找出用户要买的商品

SecKillGoods goods = secKillGoodsDao.findOne(goodsId);

//如果有这么多库存

if(goods.getRemainNum()>=num){

//模拟网络延时

Thread.sleep(1000);

if(goods.getRemainNum()>0) {

//先减去库存

int i = secKillGoodsDao.reduceStock(goodsId, num);

if(i!=0) {

//保存订单

secKillService.generateOrder(consumer, goodsId, num);

return "购买成功";

}else{

return "购买失败,库存不足";

}

}else {

return "购买失败,库存不足";

}

}

return "购买失败,库存不足";

}

在看看运行情况

高并发下的下单功能设计_第3张图片

订单表:

高并发下的下单功能设计_第4张图片

在高并发问题下的秒杀情况,即使存在网络延时,也得到了保障。

你可能感兴趣的:(高并发下的下单功能设计)