电商秒杀模块设计

电商秒杀模块设计,第1张

文章目录
  • 前言
  • 一、思路设计与技术实现
  • 二、秒杀模块代码实现
    • 1 简易入门秒杀Demo
      • 后台接口
      • 模拟大量并发
      • PS:实践中发现的问题
    • 2 扩展代码1:AQS队列加入
    • 3 引入中间件的思考
    • 4 认真一拳:引入完整的Cloud完成编码
  • 总结

前言

秒杀模块设计

面试中的非常高频率的问题,秒杀模块的设计也是考察了程序员对于高并发的处理能力,在电商项目中也是非常热门的存在,一般需要考虑的因素有以下几点:

  1. 库存;
  2. 时间限制;
  3. 安全设计 - 拦截恶意请求;

通过以上几点完成整体的思路设计与分析。

一、思路设计与技术实现

在上述的简图中,我们已经进行了一个初步的业务流程窥探,接下来是将每块逻辑通过相应的技术栈去进行代码实现。

我将代码层面拆分成大致四个阶段:

  1. 购买前检查
    • 账户是否登录:一般通过用户携带的token进行判断;
    • 是否已到了秒杀时间;
    • 商品目前的库存情况:存放缓存提高效率,注意数据一致性问题;
    • 是否限购;
  2. 开始秒杀
    • 秒杀点击:使用AQS队列存放用户id保证效率及原子性的实现;
    • 库存扣减:利用Redis原子性进行库存扣除;
    • 订单号生成:可以使用推特的雪花算法实现分布式环境id唯一;
  3. 付款
    • 如未在规定时间内购买:库存恢复,队列释放,秒杀继续;
    • 已购买:通知用户购买成功;
  4. 购买成功
二、秒杀模块代码实现 1 简易入门秒杀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.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我的思路是:

  1. 在商品库存上,使用原子类及静态关键字去防止因为多线程导致的问题;
  2. 假设当前商品种类只有一个,那么库存及购买成功的用户应该是成正比的;

目前这个测试代码先初步模拟了秒杀的步骤,接下来进行往外扩展。

PS:实践中发现的问题
  1. 秒杀接口的数据一致性问题漏洞 - 并非能用原子类这么简单的解决;
  2. 分布式唯一id问题 - 毫秒级别的压测下如果不加以盐值计算,id重复并非不可能;
  3. 从缓存获取库存数目的效率问题 - 每个线程都通过redis去获取库存数目,那效率实在太低了;
2 扩展代码1:AQS队列加入

在上述的问题中也提到,如果使用原子类作为库存数目,一旦库存减少的那段代码延时过大,立刻就会导致库存超卖的数据一致性问题,此时加入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)

不写了不写了。。。

总结

不知道从哪里看到的,电商的秒杀流程。

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/800254.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-06
下一篇 2022-05-06

发表评论

登录后才能评论

评论列表(0条)

保存