一、难点及解决方案总结

作为一个秒杀系统,常常面临以下问题:

  • 秒杀还未开始,就有用户模拟数据提前发送请求进行秒杀或者抢购开始瞬间有人直接利用脚本大量发送请求

  • 秒杀前用户不停刷新页面

  • 秒杀开始瞬间请求数暴增

  • 商品被同一用户重复秒杀

  • 超卖超买

  • 订单的持久化

解决方案:

  1. 在后端先判断服务器时间是否在秒杀时间内,如果不是,返回错误信息
  2. 使用CDN对静态资源进行分发
  3. 在秒杀开始前,提前将所有的商品信息、库存存入redis中,用户直接从redis中读取商品信息。
  4. 在redis中对每一个商品,使用商品id作为key,用户id作为value创建一个set集合暂时保存成功的订单信息,用户发送购买请求时,先查看redis中是否已经存在了该用户的订单信息,如果存在返回“重复秒杀”错误。
  5. 如果没有“重复秒杀”错误,使用分布式乐观锁进行减库存和增加订单。
  6. 当订单数每达到500的倍数,就开启4个线程对订单进行持久化操作。使用redis不能保证数据的强一致性,如果需要保证数据的强一致性,还是需要使用mysql。

二、系统实现

1、防止提前秒杀

防止提前秒杀是秒杀系统必须考虑的因素,如果商品还没开始出售就被人抢光或者有人直接利用脚本大量发送请求破坏秒杀抢购的公平性会及其影响用户的体验。这里我们采用服务器端判断时间的方式来判断是否能进行秒杀,判断流程如下:

  1. 用户发送请求
  2. 判断接收到请求的时间是否在秒杀商品抢购的时间范围内
  3. 在时间范围内返回经过加密的md5,不在则返回错误信息
  4. 用户根据返回的md5再次发送请求,服务器判断md5是否正确来判断是否能进行抢购

代码如下:

public class SeckillServiceImpl implements SeckillService {public Exposer exportSeckillUrl(long seckillId) {//从redis中读取商品信息Seckill seckill = redisDao.getSeckill(seckillId);if (seckill == null) {//缓存里没找到,去数据库找seckill = seckillDao.queryById(seckillId);if (seckill == null){return new Exposer(false,seckillId);}else {redisDao.putSeckill(seckill);}}Date startTime = seckill.getStartTime();Date endTime = seckill.getEndTime();Date cur = new Date();//不秒杀时间内if (cur.getTime() > endTime.getTime() || cur.getTime() < startTime.getTime()){return new Exposer(false,seckillId,cur.getTime(),startTime.getTime(),endTime.getTime());}//在秒杀时间内,返回md5String md5 = getMD5(seckillId);return new Exposer(true,md5,seckillId);}private String getMD5(long seckillId){//根据商品id生成md5String base =  seckillId + "/" + slat;String md5 = DigestUtils.md5DigestAsHex(base.getBytes());return md5;}
}

2、分布式乐观锁实现秒杀

我们在redis中通过乐观锁来实现线程的安全,解决超买超卖问题,乐观锁的实现如下:

public class RedisDao {public boolean lock(String seckillId,String value){try {Jedis jedis= jedisPool.getResource();try {String key = "seckillId:" + seckillId;//该锁没有被拥有,直接获得锁if (jedis.setnx(key,value) != 0)return true;String cur = jedis.get(key);//锁获得超时,强制释放锁if(!StringUtils.isEmpty(cur) && Long.parseLong(cur) < System.currentTimeMillis()){String oldValue =  jedis.getSet(key,value);//多个线程同时争抢锁,只能有一个线程获得成功if (!StringUtils.isEmpty(oldValue) && oldValue == cur)return true;}}finally {jedis.close();}}catch (Exception e){LOG.error(e.getMessage(),e);}return false;}public void unlock(String key,String value){try {Jedis jedis = jedisPool.getResource();try {String oldValue = jedis.get(key);if (!StringUtils.isEmpty(oldValue) && oldValue.equals(value)){jedis.del(key);}}finally {jedis.close();}}catch (Exception e){LOG.error(e.getMessage(),e);}}
}

分布式乐观锁整体通过CAS来实现,执行流程如下:

  1. 先执行setnx()方法,如果key不存在就设置key,返回true表示获得锁,否则执行2
  2. 判断value的时间是否过期,如果过期,通过getset()方法返回旧值。如果旧值等于1获得的value,则说明抢到了锁,返回true,如果不等于说明有其他线程先抢到了锁,返回false。

    unlock()没什么好讲的,把K-V删除就行了。

3、执行秒杀

执行秒杀前需要进行两个判断,先判断MD5是否正确,不正确不予处理。然后用用户的id和商品的id去redis中查找是否存在了秒杀订单,如果存在则返回重复秒杀错误。

上面两个判断都正确后,尝试获得锁,如果没获得锁则返回,如果获得锁了,就在redis中获取库存,如果库存>0,减库存,加订单。

每当存入500个订单时,就开启四个线程来持久化订单,如果CountDownLatch判断是否结束。

代码如下:

public class SeckillServiceImpl implements SeckillService {private static final Log LOG = LogFactory.getLog(SeckillServiceImpl.class);private final static int TIMEOUT = 5 * 1000;private static CountDownLatch countDownLatch = new CountDownLatch(4);@Autowiredprivate SeckillDao seckillDao;@Autowiredprivate RedisDao redisDao;@Autowiredprivate SuccessKilledDao successKilledDao;public SeckillExecution executeByRedis(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {if (md5 == null || !md5.equals(getMD5(seckillId)))throw new SeckillException("seckill data rewrite");try {Set<String> set = redisDao.getSuccesskill(seckillId);//判断是否重复购买if (set.contains(seckillId + "," + userPhone))return new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);long time = System.currentTimeMillis() + TIMEOUT;boolean lock = redisDao.lock(String.valueOf(seckillId), String.valueOf(time));//没获得锁if (!lock) {return new SeckillExecution(seckillId, SeckillStateEnum.WAIT_FAIL);}Integer stockNum = redisDao.getStock("stock:" + seckillId);//redis中没有库存信息,去数据库中读if (stockNum == null) {Seckill seckill = seckillDao.queryById(seckillId);stockNum = seckill.getNumber();redisDao.pushStock(seckillId, String.valueOf(seckill.getNumber()));}//库存不足if (stockNum <= 0)return new SeckillExecution(seckillId, SeckillStateEnum.FAIL);//库存-1    redisDao.pushStock(seckillId, String.valueOf(stockNum - 1));SuccessKilled successKilled = new SuccessKilled(seckillId, userPhone);//redis暂时保存订单redisDao.putSuccesskill(successKilled);int count = redisDao.getSuccesskillSize(seckillId);if (count % 500 == 0) {set = redisDao.getSuccesskill(seckillId);final List<String> list = new ArrayList<String>(set);ThreadPoolExecutor executor;//创建线程池对订单进行持久化executor = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(0));executor.execute(new Runnable() {public void run() {for (int i = 0; i < list.size() / 4; i++) {String[] info = list.get(i).split(",");successKilledDao.insertSuccessKilled(Long.parseLong(info[0]), Long.parseLong(info[1]));}countDownLatch.countDown();}});executor.execute(new Runnable() {public void run() {for (int i = list.size() / 4; i < list.size() / 2; i++) {String[] info = list.get(i).split(",");successKilledDao.insertSuccessKilled(Long.parseLong(info[0]), Long.parseLong(info[1]));}countDownLatch.countDown();}});executor.execute(new Runnable() {public void run() {for (int i = list.size() / 2; i < list.size() * 3 / 4; i++) {String[] info = list.get(i).split(",");successKilledDao.insertSuccessKilled(Long.parseLong(info[0]), Long.parseLong(info[1]));}countDownLatch.countDown();}});executor.execute(new Runnable() {public void run() {for (int i = list.size() * 3 / 4; i < list.size(); i++) {String[] info = list.get(i).split(",");successKilledDao.insertSuccessKilled(Long.parseLong(info[0]), Long.parseLong(info[1]));}countDownLatch.countDown();}});try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}redisDao.unlock(String.valueOf(seckillId),String.valueOf(time));}catch (Exception e){LOG.error(e.getMessage(),e);}return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS);}
}

三、总结

利用redis的好处就在于redis是单线程并且全部的操作都在缓存中进行,速度非常快,但是redis的缺点也很明显,就是不能保证数据的持久性,容易丢失数据,因此本文只是利用redis对秒杀系统做了一个简单的处理,真正在业务中还需要考虑其他的因素。

另外redis也可能保存失败,这时候可以利用消息队列来重新进行保存。

总之,最后我们必须保证数据的最终一致性,但在本文的实现中,并不能完全保证数据的最终一致性,这也是今后需要继续优化的地方,如果有什么好的解决方案或者有什么遗漏错误之处,欢迎大家评论!

秒杀系统架构设计与实现相关推荐

  1. 浅谈秒杀系统架构设计

    秒杀是电子商务网站常见的一种营销手段. 原则 不要整个系统宕机. 即使系统故障,也不要将错误数据展示出来. 尽量保持公平公正. 实现效果 秒杀开始前,抢购按钮为活动未开始. 秒杀开始时,抢购按钮可以点 ...

  2. 网购秒杀系统架构设计

      秒杀是电子商务网站常见的一种营销手段:将少量的商品以极低的价格,在特定的时间点开始出售.秒杀对网站的推广有很多好处,也能给消费者带来利益,但是对网站技术却是极大的挑战:网站是为正常运营设计的,而秒 ...

  3. 网购秒杀系统架构设计案例分析

    大型网站技术架构-核心原理与案例分析 作者:李智慧 申明:文章版权归作者所有,若有侵权,请联系删除 秒杀是电子商务网站常见的一种营销手段:将少量商品(通常只有一件)以极低的价格,在特定的时间点开始出售 ...

  4. 系统架构设计——网购秒杀系统架构设计

    网购秒杀系统架构设计 秒杀是电子商务网站常见的一种营销手段:将少量商品(通常只有一件)以极低的价格,在特定的时间点开始出售.比如一元钱的手机,五元钱的电脑,十元钱的汽车等.因为商品价格诱人,而且数量有 ...

  5. 网购秒杀系统架构设计案例分析——《大型网站技术架构》笔记

    一.核心思想: 网站秒杀时的并发比正常运营时多的多,所以网站的秒杀业务不能使用正常的网站业务流程,也不能和正常的网站交易业务共用服务器(否则造成巨大浪费),必须设计部署专门的秒杀系统,进行专门应对 二 ...

  6. 案例3:网购秒杀系统架构设计案例

    秒杀系统应对策略: 1.秒杀系统独立部署 2.秒杀商品页面静态化 3.租借秒杀活动网络带宽 4.动态生成随机下单页面URL 秒杀系统架构设计 1.秒杀系统页面设计尽可能简单 2.购买按钮只有活动开始时 ...

  7. 网购秒杀系统架构设计 1

    本文是「大型网站技术架构 - 核心原理与案例分析」 第 12 章的学习笔记,感兴趣的朋友可以去购买 目录: 秒杀活动的技术挑战 秒杀活动的应对策略 秒杀系统架构设计 一.秒杀活动的技术挑战 场景: 某 ...

  8. 阿里双十一秒杀系统架构设计,有哪些技术关键点?

    马上要到双11了,就来谈谈如何设计一个秒杀系统架构 技术挑战 1. 对原有业务形成冲击 秒杀活动只是网站营销的一个附加活动,特点是:时间短.并发访问量大,如果和网站原有应用部署在一起,必然会对现有业务 ...

  9. 秒杀系统架构设计与分析

    秒杀系统架构分析与实战 2016-01-18陶邦仁Qunar技术沙龙 1 秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒 ...

  10. 系统架构设计——秒杀系统架构设计

    摘要 秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见.简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程.从架构视角来看,秒杀 ...

最新文章

  1. 路由协议:RIP/OSPF/BGP—Vecloud微云
  2. 追求代码质量: 用 AOP 进行防御性编程
  3. php发送数据到视图格式_PHP-FPM的相关知识的深度解释
  4. King Gym - 102471H
  5. ubuntu ssh 免密码登陆
  6. webbrowser实现input tab事件_如何合理构造一个Uploader工具类(设计到实现)
  7. java(7)LinkedList源码
  8. 【实践篇】推荐系统之矩阵分解模型-腾讯技术
  9. 大白话讲解如何给github上项目贡献代码
  10. markdown 文档转 word
  11. Arcscan自动矢量化
  12. 正弦余弦定理,求圆弧度或度数
  13. [SLAM基础学习简记]非线性优化
  14. DataX Transformer 源码分析及 UDF 扩展与使用
  15. 知识蒸馏论文读书笔记
  16. 全球工业网络安全收入预计2028年达到244.7亿美元
  17. 17个案例印证5大生死逻辑
  18. 无穷之旅:关于无穷大的文化史 (伊莱·马奥尔 著)
  19. 工业电气自动化实习报告
  20. 《码出高效:java开发手册》读书笔记

热门文章

  1. python常用数据包
  2. Android:长度单位详解(px、dp、sp)
  3. Python异步爬虫之协程抓取妹子图片(aiohttp、aiofiles)
  4. linux内存-内存回收
  5. java封装一个类MyStock存放在cn.com.my包中。
  6. 关于Iconfont-阿里巴巴矢量图标库的使用
  7. 中国美学史中重要问题的初步探索
  8. 协方差与联合概率密度的关系、协方差的几何意义
  9. 河南省网络安全高校战队联盟CTF训练营- misc04-压缩包分析
  10. 从23岁到28岁都在做功能测试,都这个年纪还能学习自动化测试吗?