我的秒杀总结


title: 我的秒杀总结
date: 2016-11-24 19:50:35
categories:

  • 技术
    tags:
  • 实习

近期 接到一个十万数量的秒杀任务,后台完全由我写。因为之前对秒杀的处理所知甚少,故趁此机会学习了一下。当然,这里 秒杀 只涉及到 “抢” 的环节,没有下单、支付等,故对性能要求不是那么高。最终,“跌跌撞撞”地还是让系统上线了,目前运行良好。

项目环境

两台业务的服务器,还有一台缓存服务器和一台数据库服务器。
在整个项目 的 前端部分 自然离不开 负载均衡。故 这里 还是采用 Nginx 来实现。具体方式采用默认的配置,即轮询的方式。

关于“抢”的环节

intro:一天分为几个时段开抢,每个时段有几分钟的时间抢(该时段的奖品抢完了或者时间到了,该时段抢购就结束了),对于没有抢完的奖品,要求自动“滚到”下一轮。

为了满足如上要求,自然得从数据库开始设计。在系统运行前,将十万奖品 批量输入 至数据库。我给 所有奖品 设计了一个生效时间 和一个 是否被抢 的标志。

这里我采用的技巧是(解决方案):
每个奖品 都有一个生效时间,这个生效时间是每个时间段的开始时间,只有当前时间大于生 效时间该奖品才可以抢(具体可以用sql控制,这里时间采用ms,时间越往后时间越大)。

再 声明 一下抢奖品的策略:只有当前时间大于奖品的生效时间,该奖品才可以抢。这样设计解决了对于后面的几轮秒杀可以秒杀到之前没抢完的奖品(因为后面几轮的当前时间大于前面奖品的生效时间)的要求。同时 ,又可以避免 前一时段的秒杀环节 抢到 后面时段的奖品。原因自然是 当前时间比后面几轮的生效时间小。

性能方面:为了避免每一次都要去数据库查取符合条件的奖品(生效时间和是否被抢),++我自然需要将奖品一次性地多拿几个放到内存里++。这里 我使用 队列 来存放 符合条件的奖品,每来一个用户,抢走(弹出)队列头部的奖品。后面的用户来了 再弹出一个。从而避免 用户“同时”拿到同一个奖品,从而避免“超卖”现象。

那么队列我具体采用什么样的数据结构呢?如下
基于链接节点的无界线程安全队列(这个名字很霸气~~)

    private static volatile Queue<Coupon> couponQueue;
    static{
        if (null == couponQueue) {
            LOG.info("QueueServiceImpl init!!");
            couponQueue = new ConcurrentLinkedQueue<Coupon>(); 
            }
    }

具体弹出采用什么代码呢?

@Override
public  Coupon getPoll(String uid) {
    Coupon coupon = null;
    synchronized(this){
        coupon = couponQueue.poll();
        if (coupon == null) {
            setQueue();//从数据库里取奖品放入队列的方法
            coupon = couponQueue.poll();
        }
        if (coupon!=null){
            int row =  couponDao.allocateCoupon(coupon.getId(),uid,CouponConst.Status.USED,System.currentTimeMillis());

            if (row!=1){
                coupon = null;
            }
        }
    }
    //打日志
    if (coupon!=null) {
        LOG.info(
                "getPoll uid:{},couponId:{}",
                new Object[]{uid, coupon.getId()});
    }
    return coupon;
}

这里有个synchronized,是为了线程安全,避免多个人访问同一段代码产生冲突。
那么为什么把“弹出”(如果内存队列里没有了,还会去数据库里一次性取100奖品来放入内存)和update奖品是否被抢状态的代码同时锁住呢?

试想这样一种场景:内存里原本放100个奖品的队列,经过一段时间后还剩下两个奖品。此时,甲、乙两个用户过来弹出了最后的两个奖品,还未来的及修改奖品状态。此时此刻,丙冲了过来,结果发现队列里没有了,coupon==null,就去setQueue()从数据库里拿符合条件的100个奖品放入内存队列。然而!,甲乙还没来得及将其抢到的奖品修改状态,就是说这两个奖品还没有被标记已经抢过。所以 这两个奖品又会被丙放入内存队列,接着弹出!由后来者再去抢。从而造成一种现象,最后的奖品被多个用户抢到,从而形成冲突。解决方案:加锁!

是不是这样就可以了呢??

回答是No!因为我在学校 一般都写的是 一个业务服务器,这里是两个!亲,两个和一个有着巨大差别,这是放在内存里,不是缓存服务器里。所以A服务器取了100个符合条件的奖品,B服务器也取了100个符合条件的奖品,然而!,A和B可能取得都是前100个奖品,就是说,这俩服务器取得是相同的奖品,然后进行发放!怎么办呢??

解决方案:我首先想到的是将奖品队列放入缓存服务器(1台)里?这样就避免冲突。可是 缓存 用的是 xmemcached,不是redis!没法支持那么多数据结构,而且xmemcached的CAS用起来并不舒服,,,
那么,该怎么处理呢~上代码:

    int flag = -1 ;//因为 线上有两台服务器, 所以 为避免两台服务器 取 相同的 coupon 放入自己的内存;故采用 id%2==flag 分开。

    int ip = LocalIPGetter.getLocalServerIpTail(); //获取当前服务器ip
    if (ip == 223) { //如果是A服务器 取id为奇数的奖品放入内存队列
        flag = 1;
    }else {         //如果是B服务器 取id为偶数的奖品放入内存队列
        flag = 0;
    }
    List<Coupon> couponList = couponDao.selectByStatusValidate(CouponConst.Status.INIT,current,100,flag); //两台服务器根据奇偶数进行分类去数据库取奖品。

上述代码就是我的解决思路,是不是太简单了点,把数据库表分成两块,两个服务器互不干扰。

还有缓存服务器,我将获奖者名单放在里面,减少对数据库的访问。

关于“抢”的环节 多次点击问题

我遇到这样一个问题(一位用户只能秒杀成功一次):

试想这样一个场景,对于某用户甲,点击了“秒杀”按钮。但是由于网速、性能等原因,用户甲比较着急,连续点了好几次。

当第一次请求发来时:数据库里没有该用户的记录,资格认证,发现没有秒杀记录,将要添加用户,想进入秒杀流程。注意:这里是将要,还没有添加用户。此刻!!!
该用户的第二次点击请求发了过来,资格认证,发现没有秒杀记录,将要添加用户。然而!!!第一次请求添加用户成功,第二次请求又来添加,数据库主键冲突!500!

解决方案
每次点击,收到请求检查缓存没有该账户信息,第一件事放入 缓存(key值为其账户主键)中,同时设置2s后失效。
如果 缓存中 有该账户信息,说明 2s 内该用户 访问过,返回411,访问过于频繁。
OK

疏忽!

在活动开始的前一天,QA正在火急火燎地测试。自然,线上测试用的是测试数据,奖品的中奖码都是模拟的。注意,我说的是线上测试。
晚上,22:00测试完毕。我把数据库里的数据库变成真实数据,就屁颠屁颠地回家了。

我想说的是,然而!!!两台业务服务器里的两个奖品队列还是测试的模拟数据,然后我就,,,这个坑,,,
切记
切记,清空缓存,清空内存
重要事情说三遍,清空缓存,清空内存。
情空缓存,清空内存

后续的想法

我能不能将抽取、改变状态的 流程 写在数据库里,作为存储过程来调用会不会更快??
然后我就开始写存储过程,,,,

然而!!!后来DBA告诉我:

存储过程 与 触发器 都不允许。
计算尽量不要放在数据库来,
数据库主要完成数据存取的功能。

如果我这么做,有可能会导致数据库死锁,,,,

待续

我想我如果再写秒杀系统的话 自然会做的更好,以上都是我自己的摸索。后续我会尝试 消息队列(Notify?MetaQ?),用redis?什么有损服务?什么熔断机制?

继续探索吧~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值