我的秒杀总结


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?什么有损服务?什么熔断机制?

继续探索吧~

<think>好的,用户想了解黑马点评项目中秒杀功能的设计与实现总结。我需要先回忆一下相关的知识点,并结合提供的引用内容来组织回答。 首先,根据引用[1],项目是前后端分离的,前端用nginx,后端用tomcat。数据库设计可能涉及秒杀相关的表结构,比如库存、订单等。但用户主要关注的是秒杀功能的实现方案,所以需要重点分析后端如何处理高并发请求,避免超卖等问题。 接下来,引用[3]提到了Redis分布式锁,用于解决并发问题。不过文中提到的是不可重入锁,可能存在缺陷,比如不可重入、无法重试等。但秒杀场景中,通常需要高效的锁机制来控制库存扣减,可能需要用到Redis的原子操作,比如Lua脚本或者setnx结合过期时间。 另外,引用[2]提到单点Redis可能成为瓶颈,后续考虑集群策略。所以在设计秒杀时,可能需要考虑Redis的高可用和性能优化,比如使用集群或者分片。 秒杀常见的问题包括超卖、库存扣减的原子性、接口限流、防止恶意请求等。解决方案可能包括预减库存、Redis原子操作、分布式锁、异步下单、令牌桶限流等。此外,还需要考虑数据库和缓存的一致性,以及应对高并发的架构设计,比如动静分离、热点数据缓存等。 需要确认用户提供的引用是否有更多细节。比如,引用[1]提到数据库表设计,可能需要说明秒杀相关的表结构,比如秒杀订单表、商品库存表等。引用[3]的分布式锁可能用于库存扣减时的并发控制,但需要结合其他技术如Lua脚本确保原子性。 在回答时,需要分步骤说明,比如前端优化、库存预扣减、订单处理、数据一致性等。同时,结合项目的架构,比如前后端分离、Nginx和Tomcat的部署,可能涉及静态资源缓存、负载均衡等。 可能还需要提到超卖问题的解决方案,比如使用Redis的decrement操作,并检查结果是否大于等于0,或者使用Lua脚本保证原子性。此外,订单入库可能需要异步处理,通过消息队列来削峰填谷,避免直接冲击数据库。 最后,根据用户要求生成相关问题,需要围绕秒杀设计的相关技术点提问,比如库存控制、分布式锁优化、限流措施等。</think>### 黑马点评秒杀功能设计与实现总结 #### 1. **核心设计目标** - **防止超卖**:通过原子性操作保证库存扣减的准确性[^3]。 - **高并发处理**:利用Redis高性能特性缓解数据库压力[^2]。 - **安全防护**:拦截恶意请求,防止重复下单。 #### 2. **技术实现方案** ##### 2.1 库存预扣减 - **Redis原子操作**:使用`DECR`命令扣减库存,通过Lua脚本保证原子性: ```lua if redis.call('get', KEYS[1]) >= ARGV[1] then return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end ``` - **预库存加载**:活动开始前将库存从MySQL同步到Redis[^1]。 ##### 2.2 分布式锁控制 - **Redis锁实现**:通过`SETNX`+过期时间实现互斥操作[^3],例如: ```python lock_key = "seckill_lock:" + sku_id if redis.set(lock_key, thread_id, ex=10, nx=True): try: # 执行库存扣减 finally: if redis.get(lock_key) == thread_id: redis.delete(lock_key) ``` ##### 2.3 异步订单处理 - **消息队列削峰**:扣减库存成功后,将订单数据发送到RabbitMQ/Kafka,由消费者异步写入MySQL,避免数据库瞬时压力。 ##### 2.4 限流与降级 - **Nginx层限流**:在前端Nginx设置令牌桶限流(`limit_req`模块)。 - **熔断降级**:通过Sentinel监控接口QPS,超过阈值时返回友好提示。 #### 3. **关键优化点** - **热点数据缓存**:将秒杀商品信息提前存入Redis,采用`商品ID+版本号`作为键。 - **库存分段**:将总库存拆分为多个Redis分片(如`stock_1001_01`、`stock_1001_02`),降低单个键的竞争。 - **令牌验证**:用户需先获取动态令牌(加密参数)才能发起秒杀请求。 #### 4. **数据一致性保障** ```mermaid sequenceDiagram 用户->>+Redis: 请求秒杀令牌 Redis-->>-用户: 返回令牌 用户->>+Redis: 提交订单(含令牌) Redis->>+MQ: 验证令牌后发送消息 MQ-->>-MySQL: 异步消费消息写入数据库 ``` #### 5. **缺陷与改进方向** - **当前缺陷**:使用不可重入锁可能导致死锁风险 - **改进方案**:替换为Redisson可重入锁,支持看门狗自动续期
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值