- 前言
- 一、思路设计与技术实现
- 二、秒杀模块代码实现
- 1 简易入门秒杀Demo
- 后台接口
- 模拟大量并发
- PS:实践中发现的问题
- 2 扩展代码1:AQS队列加入
- 3 引入中间件的思考
- 4 认真一拳:引入完整的Cloud完成编码
- 总结
秒杀模块设计
面试中的非常高频率的问题,秒杀模块的设计也是考察了程序员对于高并发的处理能力,在电商项目中也是非常热门的存在,一般需要考虑的因素有以下几点:
- 库存;
- 时间限制;
- 安全设计 - 拦截恶意请求;
通过以上几点完成整体的思路设计与分析。
一、思路设计与技术实现在上述的简图中,我们已经进行了一个初步的业务流程窥探,接下来是将每块逻辑通过相应的技术栈去进行代码实现。
我将代码层面拆分成大致四个阶段:
- 购买前检查
- 账户是否登录:一般通过用户携带的
token
进行判断; - 是否已到了秒杀时间;
- 商品目前的库存情况:存放缓存提高效率,注意数据一致性问题;
- 是否限购;
- 账户是否登录:一般通过用户携带的
- 开始秒杀
- 秒杀点击:使用
AQS队列
存放用户id保证效率及原子性的实现; - 库存扣减:利用
Redis原子性
进行库存扣除; - 订单号生成:可以使用推特的
雪花算法
实现分布式环境id唯一;
- 秒杀点击:使用
- 付款
- 如未在规定时间内购买:库存恢复,队列释放,秒杀继续;
- 已购买:通知用户购买成功;
- 购买成功
根据上述的思路,先做一个简易版的进行猜想验证。
后台接口import com.ljm.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
public class DemoController {
/** 日志处理 */
private static Logger log = LoggerFactory.getLogger(DemoController.class);
/** 假设商品的数量 */
private static AtomicInteger productNumber = new AtomicInteger(1000);
/** 成功购买的用户 */
private static CopyOnWriteArrayList<User> users = new CopyOnWriteArrayList<>();
/**
* 秒杀接口
* @param user
* @return
*/
@PostMapping("kill")
public Boolean kill(User user) {
if (productNumber.get() <= 0) {
log.info("商品已售罄");
return false;
}
productNumber.decrementAndGet();
users.add(user);
log.info("商品剩余 = " + productNumber.get());
return true;
}
/**
* 监控
*/
@PostConstruct
private void listener() {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
while (true) {
// 持续监控 ....
try {
Thread.sleep(6000);
log.info("-------- 商品库存 = " + productNumber.get());
log.info("-------- 成功购买的用户数量 = " + users.size());
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
这个用户类写的很简单
/**
* 模拟购买商品的用户
* @author 李家民
*/
@Data
public class User {
/** 用户id */
private Long id;
}
模拟大量并发
import com.ljm.entity.User;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
public class Main {
private static Random rd = new Random();
public static void main(String[] args) throws IOException {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 3000; i++) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
long userId = rd.nextInt(900000000) + 100000000;
User user = new User(userId);
// ----
final String IP_PORT = "http://127.0.0.1:20001/kill";
PostMethod postMethod = new PostMethod(IP_PORT);
postMethod.addParameter("user", user.toString());
HttpClient httpClient = new HttpClient();
httpClient.executeMethod(postMethod);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
});
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2900);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 3000; i++) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
long userId = rd.nextInt(900000000) + 100000000;
User user = new User(userId);
// ----
final String IP_PORT = "http://127.0.0.1:20001/kill";
PostMethod postMethod = new PostMethod(IP_PORT);
postMethod.addParameter("user", user.toString());
HttpClient httpClient = new HttpClient();
httpClient.executeMethod(postMethod);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
});
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2800);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 3000; i++) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
long userId = rd.nextInt(900000000) + 100000000;
User user = new User(userId);
// ----
final String IP_PORT = "http://127.0.0.1:20001/kill";
PostMethod postMethod = new PostMethod(IP_PORT);
postMethod.addParameter("user", user.toString());
HttpClient httpClient = new HttpClient();
httpClient.executeMethod(postMethod);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
});
// ....
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
第一次验证猜想还是比较成功的。
目前这个Demo我的思路是:
- 在商品库存上,使用
原子类
及静态关键字去防止因为多线程导致的问题; - 假设当前商品种类只有一个,那么库存及购买成功的用户应该是成正比的;
目前这个测试代码先初步模拟了秒杀的步骤,接下来进行往外扩展。
PS:实践中发现的问题- 秒杀接口的数据一致性问题漏洞 - 并非能用原子类这么简单的解决;
- 分布式唯一id问题 - 毫秒级别的压测下如果不加以盐值计算,id重复并非不可能;
- 从缓存获取库存数目的效率问题 - 每个线程都通过redis去获取库存数目,那效率实在太低了;
在上述的问题中也提到,如果使用原子类作为库存数目,一旦库存减少的那段代码延时过大,立刻就会导致库存超卖的数据一致性问题,此时加入AQS的阻塞队列能够解决该问题,后期的分布式环境我们还是会使用Redis,还是先写一个demo作为思路学习。
import com.ljm.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
/**
* 秒杀接口
* @author 李家民
*/
@RestController
public class DemoController {
/** 日志处理 */
private static Logger log = LoggerFactory.getLogger(DemoController.class);
/** 商品a库存 */
private static final Integer repertoryNumber = 1000;
/** 成功抢到商品的用户 */
private static ArrayBlockingQueue<User> blockingQueue = new ArrayBlockingQueue<>(repertoryNumber);
/**
* 秒杀接口
* @param user
* @return
*/
@PostMapping("kill")
public Boolean kill(User user) {
// 阻塞队列 成功抢购到商品的用户
try {
blockingQueue.add(user);
// 延时阻塞等待插入的代码 - blockingQueue.offer(user,3, TimeUnit.SECONDS);
} catch (IllegalStateException illegalStateException) {
// 如果出现这个异常 代表队列已满
log.info("商品已被抢购一空");
return false;
}
return true;
}
/**
* 监控
*/
@PostConstruct
private void listener() {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
while (true) {
// 持续监控 ....
try {
Thread.sleep(6000);
// log.info("-------- 商品库存 = " + "null");
log.info("-------- 成功购买的用户数量 = " + blockingQueue.size());
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
后续将引入各个中间件进行分布式的联动。
3 引入中间件的思考既然是引入其它中间件来辅佐秒杀,那么流程这块的技术栈需要我们重新梳理一下。
通过目前想到的这些点进行编码工作。
4 认真一拳:引入完整的Cloud完成编码算了,本人比较懒,先这样。
import org.springblade.activity.entity.Product;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.tool.api.R;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 秒杀模块控制器
* @author 李家民
*/
@RestController
@RequestMapping("SecKillGoodsController")
public class SecKillGoodsController {
/**
* 假象这个接口参数
* 1.用户对象 - 购买前需要用户事先登录
* 2.商品对象 - 是否可购买(普通商品/秒杀商品/预售商品) - 通用的购买接口
* public XXX XXX(User user,Product product){}
* 在进行商品购买时 提前判断商品属性 普通商品or秒杀商品
* ------ 分割线 ------
* 秒杀前的准备工作
* 1.缓存预热
* 2.链接动态加密
* 3./
* @param bladeUser 用户对象
* @param product 商品对象
* @return 返回参数
*/
@PostMapping("payProduct")
public R payProduct(BladeUser bladeUser, Product product) {
// 总共就三个流程:购买前 购买中 购买后
if(bladeUser == null){
return R.fail("请先登录");
}
return R.status(true);
}
}
文章目录 |
---|
电商网站中,50W-100W高并发,秒杀功能是怎么实现的? - 知乎 (zhihu.com) |
不写了不写了。。。
总结不知道从哪里看到的,电商的秒杀流程。
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)