目录
初步思考
秒杀活动
订单防止超卖
订单超时如何处理
原文地址
前端:页面尽可能静态化,css/js合并,减少请求数
扩容:增加机器,提高处理请求能力
限流:应用限流(nginx,tomcat设置线程池,最大请求数),服务限流(限流算法,令牌桶/漏桶),MQ堆积消息,用户请求限制(单位时间内访问接口次数)【主要保证每台机器能够处理自己能力之内的任务】
使用多线程开发,提高机器处理能力
动静分离
1、数据拆分
动静分离的首要目的是将动态页面改造成适合缓存的静态页面;第一步就是分离出动态数据,主要从以下两方面进行:
2、静态缓存
剩下的数据是静态数据,例如商品信息,商品图片,页面js/css等等,进行合理的缓存(生成静态页面)
静态数据缓存到哪里?浏览器,CDN,服务端
用户从CDN获取到静态数据
秒杀一般在12306抢票或者电商举行的一些活动时遇到。对于一些稀缺或特价商品,电商网站一般会在约定时间点,在秒杀页面对其进行限量销售。
秒杀相关特点:
从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。
设计流程
1、限流
2、将库存放到redis中、接收用户请求的时候。从redis取库存,判断库存量是否大于本次订单购买量
库存大于本次购买量:扣减redis中的库存、并且将订单信息推送到MQ;
库存小于本次购买量:直接返回、数量不足。
3、MQ消费者获取消息:
1):更新数据库库存(乐观锁)
2):生成订单信息,扣除用户账户的订单金额(余额不足的话、将本次购买量加回到库存里)
3):异步通知用户购买结果。
原文地址
令牌机制实现秒杀业务(推荐)
利用定时任务将某些商品在规定时间之后要开启秒杀,根据库存量同步到Redis中。根据每一个商品产生对应的token数量,采用Redis中的List数据类型存储每个商品的令牌。(采用List数据类型存储的原因主要是每一个线程从List中pop时是单线程处理的,所以每一个线程拿到的令牌就不可能是同一个了)
在秒杀期间,每一个用户都去获取对应商品中的令牌(判断令牌是否有效-非空判断,如果是已经被支付过的token就不能再次被抢购)
超时未支付,归还令牌 支付成功,产生订单,标记当前令牌已经被支付,同时删除当前令牌。
原文链接
现象
原因
数据库底层的写操作和读操作可以同时进行;写操作默认带有隐式锁,但是读操作默认是不带锁的;所以当用户1去修改库存的时候,用户2依然可以都到库存为1,所以出现了超卖现象。
方案分析
(1)Sql限制
在更新数据库减少库存时,进行库存限制条件(排他锁,也就是写锁起到了作用)
并采用数据库乐观锁(version版本号,CAS原理),如果限定条件不满足或版本号不一致就无法成功;
update table set count=count-1,version=versiono+1 where version=version and (count-1)>0;
(2)使用Redis对列实现
将要促销的商品以对列的方式存入Redis中;每当用户抢到一件商品则从队列中删除一个数据,以确保商品不会超卖。(将多线程变为单线程读写)
(3)使用Redis原子性操做命令(decr,incr),移除后同步更新数据库记录;
(1)数据库加唯一索引限制
对于生成多个相同订单,在UserId和OrderId上加唯一索引。
原文地址
在开发中往往会遇到一些延时任务的需求
延时任务和定时任务的区别在哪里呢?
方案分析
(1)数据库轮询
通过线程定时的去扫描数据库,通过订单时间来判断是否有超时订单,然后进行操作。
使用场景:小型项目
缺点:存在延迟(定时任务扫描的间隔),严重影响数据库性能(假如数据量过大,每几分钟扫描一次,影响其他业务)
(2)JDK延时对列
JDK自带的DelayQueue,是一个无阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。放入DelayQueue中的对象,是必须要实现Delayed接口的。
方法:
package com.test.thread;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class OrderDelay implements Delayed {
private String orderID;
private long timeout;
public OrderDelay(String orderID, long timeout) {
this.orderID = orderID;
this.timeout = timeout;
}
/* 返回距离你自定义的超时时间还有多少 */
@Override
public long getDelay(TimeUnit unit) {
return timeout - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
if (o == this) {
return 0;
}
OrderDelay t = (OrderDelay) o;
long d = getDelay(TimeUnit.SECONDS) - t.getDelay(TimeUnit.SECONDS);
return d > 0 ? 1 : 0;
}
public void execute() {
System.out.println(orderID + "\t订单要被删除啦");
}
public static void main(String[] args) {
LinkedHashMap contains = new LinkedHashMap<>();
contains.put("order_0", 5);
contains.put("order_1", 4);
contains.put("order_2", 10);
contains.put("order_3", 12);
DelayQueue queue = new DelayQueue<>();
System.out.println("任务添加到延时队列中");
for (Map.Entry entry : contains.entrySet()) {
queue.put(new OrderDelay(entry.getKey(), (1000 * entry.getValue() + System.currentTimeMillis())));
}
long start = System.currentTimeMillis();
while (true) {
try {
queue.take().execute();
System.out.println("执行时间:\t" + (System.currentTimeMillis() - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
优点:效率高,任务触发时间延迟低
缺点:数据存储在内存中,服务器重启,数据丢失;内存限制(订单过于庞大);
(3)时间轮算法
用Netty的HashedWheelTimer实现
package com.test.thread;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask {
boolean flag;
public MyTimerTask(boolean flag) {
this.flag = flag;
}
@Override
public void run(Timeout timeout) throws Exception {
System.out.println("删除订单");
this.flag = false;
}
}
public static void main(String[] args) {
MyTimerTask timerTask = new MyTimerTask(true);
Timer timer = new HashedWheelTimer();
timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
int i = 1;
while (timerTask.flag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + "秒过去了");
i++;
}
}
}
优点:效率高,代码复杂度比DelayQueue低,任务触发时间延迟比DelayQueue低
缺点:数据存储在内存中,服务器重启/宕机,数据丢失;内存限制(订单/任务数过多);
(4)Redis缓存
Redis的zset是一个有序集合,每一个元素都关联了一个score,通过score排序来获取集合中的值。
实现:将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时。
(5)使用消息队列
RocketMQ延时消息(只支持特定级别的延迟消息,1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h)
RabbitMQ有延时对列