# 分布式秒杀系统 **Repository Path**: miracleSam/eureka-cluster ## Basic Information - **Project Name**: 分布式秒杀系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 3 - **Created**: 2021-04-02 - **Last Updated**: 2023-03-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 分布式秒杀系统 ## 技术栈 ### 框架 - 微服务 springboot+maven - 数据库 mybatis-plus - 负载均衡 springCloud/Nginx ### Java - 线程池 ThreadPoolExecutor - Redis 实现分布式session - JVM锁 ReentrantLock/Synchronized/AOP切面锁 - 数据库锁 乐观锁/悲观锁 - 分布式锁 Redis锁框架Redison/Zookeeper锁 - JVM消息队列 LinkedBlockingQueue/ConcurrentLinkedQueue - 分布式消息队列 kafka ## 优化方案 - 缓存静态页面 - Nginx流量分发 - Redis缓存提高并发量 - kafka异步下单 - 数据库、消息队列、微服务高可用 - 前端限流 ### 软件架构 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0421/112324_ad1a70b1_1515186.png "屏幕截图.png") ### 安装教程 1. 在provider1、provider2目录下的application.yml中配置端口、数据库、redis、kafka、eureka服务注册地址 2. 在server1、server2下配置application-peer.yml中配置对等实体 3. 导入resource目录下的sql文件 4. enjoy ## 登录处理 ### 单机 1. 为每个用户生成UUID 2. HttpServletRequest从cookie获取UUID,HttpServletResponse将UUID写入cookie中 ### 分布式session 客户端发送一个请求,经过负载均衡后该请求会被分配到服务器中的其中一个,由于不同服务器含有不同的web服务器(例如Tomcat),不同的web服务器中并不能发现之前web服务器保存的session信息,就会再次生成一个JSESSIONID,之前的状态就会丢失。 1. springSession 2. 将UUID放入redis中 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0421/111257_8f4db62d_1515186.png "屏幕截图.png") ## 秒杀案例 ### 单机环境 #### 秒杀1(出现超卖) ReentrantLock 服务层方法加上@Transactional注解,lock放在方法中 **问题:** lock与事务冲突,lock在事务单元中执行,某个事务未提交前锁已经释放,下一个事务脏读 **解决方案:** 锁上移包裹整个事务单元,或者手动提交事务后释放锁 #### 秒杀1 AOP锁 QPS 30 1、自定义注解@ServiceLock 2、切面绑定@ServiceLock,将注解用在服务层方法上,连接点为该方法 #### 秒杀2 数据库悲观锁 QPS 42 - for update 行锁 - 表锁(未采用) #### 秒杀3 数据库乐观锁 QPS 26 - 更新库存时判断new stock==stock-1 - 版本号 1. 数据库表加入version字段(**int,Integer,long,Long,Date,Timestamp,LocalDateTime**) 2. 取出记录时,获取当前version 3. 更新时,带上这个version 4. 执行更新时, set version = newVersion where version = oldVersion 5. 如果version不对,就更新失败 [mybatis-plus实现:@Version+乐观锁拦截器OptimisticLockerInnerInterceptor](https://mp.baomidou.com/guide/interceptor-optimistic-locker.html#optimisticlockerinnerinterceptor) #### 秒杀4 单例阻塞队列 QPS 61 所有请求都生成Order,放入队列中 ##### BlockingQueue take() 与带超时时间的poll类似,不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒 offer(e) 队列未满时,返回true;队列满时返回false。非阻塞立即返回。 //类级的内部类,只有调用时才延迟加载 private static class SingletonHolder{ /** * 静态初始化器,由JVM来保证线程安全 */ private static SeckillQueue queue = new SeckillQueue(); } ##### 消费类SeckillQueueCunsumer 1. 实现ApplicationRunner接口,程序运行时启动 2. 开启线程消费BlockingQueue的订单 ##### 问题:队列大小要根据业务合理设置 #### 秒杀5 Redis缓存优化+阻塞队列 QPS 1071 ##### 思路: 1. 请求查找缓存,缓存未命中则查数据库,更新缓存 2. 如果库存不足,直接返回 3. 更新数据库,更新成功后删除缓存(旁路缓存原则) [数据不一致问题:设置过期时间](https://blog.csdn.net/dabaoshiwode/article/details/115410630) #### 秒杀6 Redis+kafka异步下单 QPS 1281 ##### kafka订单对象序列化问题 1. 自定义序列化工具类,实现Serializer接口 2. 采用fastjson将对象转换为字符串,使用默认的StringSerializer序列化,消费端使用fastjson反序列化字符串 ### 分布式环境 分布式环境下请求会转发到多个服务实例,每个服务的JVM锁都是不一样的,所以导致了ReentrantLock、Synchronized失效的情况。 #### 解决方案: 1. 数据库锁 2. 分布式锁 #### 秒杀1 Redisson锁+kafka异步下单 QPS 84 Nginx(2个服务) 186 ##### 原理: 相当于调用redis的setnx命令设置(lockname,lock,过期时间)的键值对,如果设置成功视为获取到锁,业务处理完后删除键值对,删除成功视为释放锁。但这种方式存在问题: 1. 设置的超时时间小于业务执行时间,锁过期;过期时间不好把握 2. 业务出现bug,没有释放锁,出现死锁 ##### 看门狗机制(watch dog): 1. 自动续期,如果业务超长,运行期间自动续上30s,不用担心业务时间长,锁自动过期被删掉 2. 超时自动释放锁 #### 秒杀2 Redis原子递减+kafka QPS 652 Nginx(2个服务) 1340 Lua脚本实现递减操作的原子性 ##### 思路: 1. 将秒杀商品的库存预热加载到redis中,存储格式 goodsId:goodsStock 2. goodsStock小于1直接返回 3. 原子递减,操作成功生成订单发送到kafka的topic中 4. 消费者消费订单,入库 #### 秒杀3 Zookeeper锁+kafka Nginx(2个服务) 350 ##### CAP理论:一致性,可用性,分区容错性 redis集群满足AP 单机CP zk集群满足CP - redis集群加锁后马上返回客户端结果,然后同步(牺牲一致性) - zk集群先保证半数节点同步成功,再返回加锁结果(牺牲可用性) ##### 选择策略 优先满足高并发选择redis,优先满足高可用选择zk ## 限流与负载均衡 ### Sentinel ##### 重要概念 Resource sentinel通过资源来保护业务代码或后方服务,使用@SentinelResource注解定义资源(指定 `blockHandler` 和 `fallback` 方法),FlowRule定义规则,然后在程序中埋点实现服务的保护。 - try-catch 方式(通过 `SphU.entry(...)`),当 catch 到BlockException时执行异常处理(或fallback) - if-else 方式(通过 `SphO.entry(...)`),当返回 false 时执行异常处理(或fallback) Slot 插槽链通过一定的编排顺序来达到最终的限流目的。 Entry Sentinel 中用来表示是否通过限流的一个凭证,每次执行 `SphU.entry()` 或 `SphO.entry()` 都会返回一个 `Entry` 给调用者,意思就是告诉调用者,如果正确返回了 `Entry` 给你,那表示你可以正常访问被 Sentinel 保护的后方服务了,否则 Sentinel 会抛出一个BlockException(如果是 `SphO.entry()` 会返回false),这就表示调用者想要访问的服务被保护了,也就是说调用者本身被限流了。 ##### 实现思路 根据每个服务的负载能力,设定流量极限。 [SpringBoot 2.0 + 阿里巴巴 Sentinel 动态限流实战](https://blog.52itstyle.vip/archives/4395/) ### Nginx Nginx采用了Linux的epoll模型,epoll模型基于事件驱动机制,它可以监控多个事件是否准备完毕,如果OK,那么放入epoll队列中,这个过程是异步的。worker只需要从epoll队列循环处理即可。 ##### 反向代理 ![img](https://pic2.zhimg.com/v2-4787a512240b238ebf928cd0651e1d99_b.jpg) ##### Master-Worker模式 ![img](https://pic4.zhimg.com/v2-b24eb2b29b48f59883232a58392ddae3_b.jpg) ##### 实现思路 Nginx采用轮询策略实现负载均衡,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。 根据每个服务的负载能力,采用加权轮询指定轮询几率。 分布式session问题:每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。 ### SpringCloud ##### Eureka server1,server2使用@EnableEurekaServer启用对等服务注册中心,provider1,provider2向注册发送心跳。 consumer服务使用@LoadBalance注解实现负载均衡。实例代码: ``` @SpringBootApplication @RestController @EnableEurekaClient public class ConsumerApplication { @Autowired RestTemplate restTemplate; public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @LoadBalanced @Bean public RestTemplate rest() { return new RestTemplate(); } @GetMapping(value = "/callHello") @ResponseBody public String hello(){ /** * 小伙伴发现没有,地址居然是http://service-provider * 居然不是http://127.0.0.1:8082/ * 因为他向注册中心注册了服务,服务名称service-provider,我们访问service-provider即可 */ String data = restTemplate.getForObject("http://service-provider/hello",String.class); return data; } } ``` ##### 与Nginx的区别 ![与Nginx的区别](https://images.gitee.com/uploads/images/2021/0421/111322_44b48dda_1515186.png "屏幕截图.png") ## JVM调优案例 ![JVM调优案例](https://images.gitee.com/uploads/images/2021/0421/111510_947716f7_1515186.png "屏幕截图.png")