如何建立秒杀系统

如何建立秒杀系统?

一、秒杀系统场景

一个系统好坏的衡量可以从3个方面进行,用户、时间和空间。用户包括UI界面设计、交互设计以及用户并发量。时间包括系统响应时间、吞吐量。空间则是资源利用效率。

秒杀活动基于对限量商品的买卖,往往会形成短时间的高并发的情况。但最后只有少部分人能秒杀成功。

秒杀活动可以分为3个阶段:

  • 秒杀前:用户不断刷新页面,向后台提交请求
  • 秒杀时:大量并发消息涌入后台
  • 秒杀后:大量订单处理,还有人退单

在这个过程中,最重要的是**!扣减库存!**,必须保证库存扣减的逻辑性。

二、线程安全

多个用户的请求以多线程的方式进行,多线程共享商品剩余数量amount。

如果不使用线程安全的技术,往往会造成实际购买数量大于存货的情况。我们用CountDownLatch来模拟一下这种情况。

public class Main implements Runnable{
	private static int threadNum = 1000; // 一共有1000个并发的请求
	private final static CountDownLatch startSignal = new CountDownLatch(threadNum);
	private static int amount = 2; //总共商品数量
	private static int success = 0; //卖出的数量
    public static void main(String[] args) throws InterruptedException{
		Main task = new Main();
    	for(int i = 0;i < threadNum;i++) {
    		new Thread(task).start();
    		startSignal.countDown(); // 用CountDownLatch实现多个线程同时start
    	}
    	Thread.sleep(50);
    	System.out.println(success);
    }

	@Override
	public void run() {
		try {
			startSignal.await();
			if(amount > 0) {
				System.out.println(Thread.currentThread().getName() + "秒杀成功,剩余商品数量" + --amount);
				success++;
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}


让我们打印输出结果来看看。

Thread-999秒杀成功,剩余商品数量0
Thread-4秒杀成功,剩余商品数量-5
Thread-2秒杀成功,剩余商品数量-4
Thread-5秒杀成功,剩余商品数量1
Thread-3秒杀成功,剩余商品数量-1
Thread-6秒杀成功,剩余商品数量1
Thread-0秒杀成功,剩余商品数量-2
Thread-1秒杀成功,剩余商品数量-3
8

哦,我的天!商品数量只有2个,但是却卖出了8个,真的是酿成大错!

这就是线程不安全,说的专业点就是

不提供数据访问保护,在多线程环境中对数据进行修改,会出现数据不一致的情况。

如何保证数据被保护呢?
可以有三种方式:
1、数据库锁;2、synchronized等;3、缓存;

三、数据库锁

数据库中保证线程安全,我们首先会想到加锁。在数据库中,锁按照范围分成行锁和表锁,按照性质分成排它锁和共享锁

类型性质
行锁只锁住一行的锁,在锁住的时候,其他线程不能再对它进行访问。
表锁锁住整个表,在锁住的时候,其他线程不能再对它进行访问。
排它锁其他线程无法再对它加锁
共享锁只能读,不能修改。其他线程可以对它加共享锁

根据上表,排列组合一些就知道一共有4种情况,行排他锁、表排它锁、行共享锁、表共享锁。我们用Mysql来看看这到底是怎么一回事。

首先,Mysql中加共享锁是用lock in share mode,加排他锁是用for update

首先我们先将事务自动提交关闭,否则mysql会认为每一句sql语句都是事务,就比较难模拟并发的情况了。

mysql> set autocommit=off;

建立book_info,并开启两个会话对其进行处理。

mysql> BEGIN;
mysql> SELECT * FROM book_info;
+---------+-----------+--------------------+-----------------------+---------------+-------+----------+-------+
| book_id | name      | author             | publish               | ISBN          | price | class_id | count |
+---------+-----------+--------------------+-----------------------+---------------+-------+----------+-------+
|    1111 | 活着      | 余华               | PP                    | 9787506365437 | 20.00 |    10001 |    10 |
|    1234 | Jane      | Amy                | PP                    | 123           | 23.12 |      102 |    23 |
|    2323 | 小王子    | 圣埃克苏佩里       | 人民文学出版社        | 9787020042494 | 39.50 |    10001 |    20 |
+---------+-----------+--------------------+-----------------------+---------------+-------+----------+-------+
3 rows in set (0.00 sec)

在会话1中,对某一行加排他锁。也就是book_id=1111的一行。

mysql> SELECT * FROM book_info where book_id=1111 for update;
+---------+--------+--------+---------+---------------+-------+----------+-------+
| book_id | name   | author | publish | ISBN          | price | class_id | count |
+---------+--------+--------+---------+---------------+-------+----------+-------+
|    1111 | 活着   | 余华   | PP      | 9787506365437 | 20.00 |    10001 |    10 |
+---------+--------+--------+---------+---------------+-------+----------+-------+
1 row in set (0.00 sec)

此时在会话2中,如果想对这一行加锁或者增删改,会发现一直处于阻塞状态。直到会话1中commit为止。且任何涉及该行的操作都不能进行(比如选择author=余华也无法进行)。

mysql> select * from book_info where book_id=1111 for update;

对lock in share mode的模拟就不在这里贴出了。效果就是,多个会话可以同时访问某行,但是无法对改行进行修改。

对某一行加了锁,并不表示所有针对改行的操作都无法进行了!
最基本的select * from table;是可以进行的,注意后面我们没有主动要求上锁。
因为Mysql中,只有设计增删改的操作需要得到对象的锁,只是单纯的查是不需要获得锁的。

四、同步机制

用synchronized对方法或者代码块加锁。

public synchronized void run() {
    // 之前的内容
}    

也就是说,在进行run()的时候必须得到锁,且一次只能有一个线程得到这把锁,保证了数据不会出错。

Thread-0秒杀成功,剩余商品数量1
Thread-999秒杀成功,剩余商品数量0
2

在java中,除了synchronized还有Lock。

此处就不得不说乐观锁、悲观锁了。
数据库可以通过增加version的方式实现乐观锁。

此处未完待续。。。

但是这两者的问题在于,锁太重了,处理速度太慢,无法抗住百万级别的流量访问。且流量直接打到后端,可能会造成系统的崩溃。
所以现在除了秒杀系统一般通过分层的思想。

五、秒杀系统

一般如下分层。

来自网站,侵删(来自网站,侵删)

虽然秒杀系统流量很高,但实际有效的是十分有限的。比如用户在秒杀前多次刷新,以及许多没买到票的请求完全不必直接作用在数据库上。所以我们每层都拦截无效的流量,可以有效地避免高并发可能造成的崩溃。

1. CDN Cache

缓存是系统快速响应中的一种关键技术,是一组被保存起来以备将来使用的东西。往往存在于高速部件与低速部件之间,例如CPU与内存之间存在着CPU缓存。

CDN全称Content Delivery Network。即在各地部署多套静态存储服务,本质上是空间换时间。

比如说,我去访问一个页面,我对服务器发出请求之后,服务器从数据库中调取内容,然后发送给我。如果一共有10000个请求,那么服务器要调取10000次内容。而数据库访问是比较慢的。所以这会使得系统响应速度非常慢。

有了CDN之后,将网页内容放在CDN节点的缓存中,访问时自动选择最近的节点内容,不存在再请求原始服务器。而缓存的访问速度是非常快的,从而实现了快速响应。并且不同的请求可以都直接读取缓存的内容,所以高并发也没问题。

CDN适合存储更新很少的静态内容,文件更新慢。

所以在我们的秒杀系统中,尽量将秒杀商品的页面设计成静态的,
除了秒杀按钮需要服务端进行判断,其他静态数据存储在CDN和浏览器缓存中。如此一来,多用户不断刷新就导致流量直接进入服务器。

其实在这一层之后,可以对同一个IP的访问次数做出限制。
比如一个IP的流量只能往后台一次,或者设置最小时间间隔。
防止刷票系统多次刷票。

2. Redis读写分离

Redis是单进程单线程的网络模型,处理所有的客户端连接请求,命令读写请求。
Redis提供的所有API操作,相对于服务端方面都是one by one执行的,
命令是一个接着一个执行的,不存在并行执行的情况。

我们将amount写入Redis队列,每一次访问是的amount数量-1。当amount数量为0之后,立即拦截所有的流量。

最后,下单请求只有一少部分可以被接受。

此处需要加入服务器集群。
3. Redis主从版

下单成功之后,进行信息校验。

依旧避免直接对数据库的访问。入库的时候,如果订单量比较大,直接写入数据库还是比较慢,所以利用Redis的消息队列组件,异步处理订单入库。只要内容进入了消息队列,就认为下单成功。

啊啊啊啊,Redis还有好多内容待补充!+异步

4. 数据控制

通过数据控制,在Redis读写分离时拦截大部分的请求。之后再确认订单之后,放入消息队列,异步下单。最后写入数据库。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值