参考了LMAX架构思想的自创12306订票的极限速度方案,无需动用事务和数据库锁_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 参考了LMAX架构思想的自创12306订票的极限速度方案,无需动用事务和数据库锁

参考了LMAX架构思想的自创12306订票的极限速度方案,无需动用事务和数据库锁

 2012/1/31 9:22:42  ahyyxx222  程序员俱乐部  我要评论(0)
  • 摘要:我的方案将实现一个订单全程只进行一次insert操作,查余票无需发送select语句,完全消除由于事务、库表锁行锁带来的性能损耗。流程:1、将一趟车的所有票况在系统启动时存入一个该趟车专有类中,票况信息为单例。类中有4个静态的余票数组(站票、坐票、硬卧、软卧),存储该趟车各途经站点下完乘客后的剩余的座位数(余票),用的是数组而不是整数是因为现在的列车座位是会被复用的。2、类中有两个主要方法,一个查余票,一个查余票并购票,查余票方法仅从本类的余票数组中获取余票数
  • 标签:数据库 数据 速度 架构

我的方案将实现一个订单全程只进行一次insert操作,查余票无需发送select语句,完全消除由于事务、库表锁行锁带来的性能损耗。

?

流程:

1、将一趟车的所有票况在系统启动时存入一个该趟车专有类中,票况信息为单例。类中有4个静态的余票数组(站票、坐票、硬卧、软卧),存储该趟车各途经站点下完乘客后的剩余的座位数(余票),用的是数组而不是整数是因为现在的列车座位是会被复用的。

2、类中有两个主要方法,一个查余票,一个查余票并购票,查余票方法仅从本类的余票数组中获取余票数,购票方法在查了票以后若有余票则扣除,无论有无余票,都会发出一条insert,保存订单号、车次、乘车日期、起始站、终点站、票的种类、购票结果(成功/失败)。

3、要买该趟车的订单被排入4个队列,分别对应站票、坐票、硬卧、软卧队列。

4、每个队列挨个取订单来调用购票方法,由于操作的是对应的4个不同余票数组,所以不会出现并发操作。

5、用户请求在被排入队列时就已返回,用户持有订单号,将在若干秒后通过ajax查订单处理结果。查结果的方式为,往之前insert的表中按订单号查结果是成功还是失败(订单号为索引)。若无则判为仍在队列中,提示等待,一定时间后ajax再来。

?

分析下性能:

由于未动用数据库锁,购票过程就是数组值操作加一条insert,所以可以非常快,相信在调优下购票业务逻辑单机对某趟车一秒钟可以处理2000订单以上,实现各趟车分库后应该还能大大提高。一趟车大约有10000张票,若同时有10万人在秒杀该趟车次,则50秒可以全部告知用户是否秒杀成功,然后用户视结果换一种票类或换一趟车继续秒杀。

当然全国一天有近4000趟车,全都这么极端实现就得去租4000台服务器了,一台装一个数据库,一个库就为一趟车服务。就算一台100元租一天,一天要花40万租服务器,那可以考虑适当整合一下,几趟车用一台数据库服务器,性能也仍可接受,毕竟高峰期持续不长。

?

再提下最重要的宕机处理:

既然实时票数存在单例的类中,那就要应付可能出现的机器挂掉的情况。借鉴事件驱动思想,我的方案是:换一台业务服务器顶上,把挂掉的各趟车的类重新初始化为初始总票数,然后从库里查出所有的已成功订单来挨个算票,重新在数组里减一遍票,就能恢复到宕机前的余票情况了。一天最多数百万成功订单,全查出来再算一遍,并不需要很长时间,宕机这情况本来就少见,实在遇上了花半小时再重算下票数不过份,何况怎么可能所有算票服务器全挂了呢。

?

?

算票业务类粗略实现如下,仅供参考,未细化验证:

import java.util.HashMap;
import java.util.Map;

// 该趟车为上海始发,途经苏州、南京、天津、终点站北京
public class TicketTest {
 // 用来保存购票结果的dao,库表结构为 订单id,起始站,到达站,票的类别,购票结果
 private TicketDao ticketDaoImpl;

 // 该趟车各类票的总数
 private final static int zhanpiao = 2000;
 private final static int zuopiao = 2000;
 private final static int yingwo = 1000;
 private final static int ruanwo = 200;
 // 该趟车从始发站开至各个站点的余票(座)
 private final static int[] zhanpiaoArr = new int[] { zhanpiao, zhanpiao,
   zhanpiao, zhanpiao };
 private final static int[] zhuopiaoArr = new int[] { zuopiao, zuopiao,
   zuopiao, zuopiao };
 private final static int[] yingwoArr = new int[] { yingwo, yingwo, yingwo,
   yingwo };
 private final static int[] ruanwoArr = new int[] { ruanwo, ruanwo, ruanwo,
   ruanwo };
 private final static Map<String, Integer> zhanming = new HashMap<String, Integer>();

 public void init() {
  // 始发站上海不用存,数组中也没有上海的余票,因为不存在从上海坐到上海这种情况
  // value值,为到达该城市余票在票数数组(如zhanpiaoArr)中的下标
  zhanming.put("苏州", 0);
  zhanming.put("南京", 1);
  zhanming.put("天津", 2);
  zhanming.put("北京", 3);
 }

 // 获取从start站到end站的余票,type为票的种类:站、坐、硬卧、软卧
 public int getRemainingTicket(String start, String end, int type) {
  int startIndex = zhanming.get(start);
  int endIndex = zhanming.get(end);
  int remainingNumber = 0;
  int[] array = new int[zhanming.size()];
  // 确定从哪类余票数组中查票,并把余票初始化为该类票最大票数
  switch (type) {
  case 1:
   array = zhanpiaoArr;
   remainingNumber = zhanpiao;
   break;
  case 2:
   array = zhuopiaoArr;
   remainingNumber = zuopiao;
   break;
  case 3:
   array = yingwoArr;
   remainingNumber = yingwo;
   break;
  case 4:
   array = ruanwoArr;
   remainingNumber = ruanwo;
   break;
  }

  // 查询从某站到某站每一站的剩余票数,以票最少的站点所剩票数为实际余票数
  // 例如卖掉了一张上海到苏州硬座,那苏州余票为1999,票数数组是[1999, 2000, 2000, 2000]
  // 此时若你要买上海到南京,则取途经站最小值,实际上只剩1999个座位
  // 而如果你想买的是苏州到北京,取途经站最小值则是剩2000张票,因为前面那张票的人在苏州下车了
  for (int i = startIndex; i <= endIndex; i++) {
   // 若找到最小值,则做为余票数
   if (array[i] < remainingNumber) {
    remainingNumber = array[i];
   }
  }

  return remainingNumber;
 }

 // 进行实时查票并购票,有4个订单队列在排队,每个队列都会逐个订单来调用此方法,因此同一种票,即同一余票数组不会被并发操作。
 public void buyTicket(String start, String end, int type, String orderId) {
  int remainingNumber = getRemainingTicket(start, end, type);
  if (remainingNumber > 0) {
   // 如果有余票,将要买的票扣除并将购买成功结果存库
   int[] array = new int[zhanming.size()];
   switch (type) {
   case 1:
    array = zhanpiaoArr;
    break;
   case 2:
    array = zhuopiaoArr;
    break;
   case 3:
    array = yingwoArr;
    break;
   case 4:
    array = ruanwoArr;
    break;
   }
   for (int i = array[zhanming.get(start)]; i < array[zhanming.get(end)]; i++) {
    // 从票数数组找到所有途经站点将票数减1,到达站不用减,因为你已下车,不会占用到达站的座位
    array[i] -= 1;
   }
   ticketDaoImpl.save(orderId, start, end, type, true);
  } else {
   // 无余票,直接将购买失败记录存库,这个不可省略,否则用户查询购票结果时将分不清是没买上还是仍在排队
   ticketDaoImpl.save(orderId, start, end, type, false);
  }
 }

 // set/get省略
 }

?

发表评论
用户名: 匿名