我的方案将实现一个订单全程只进行一次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省略 }
?