SSM(十八) 秒杀架构实践
前言
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。
本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果,文章较长请准备好瓜子板凳(liushuizhang😂)。
本文所有涉及的代码:
最终架构图:
之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。
本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果,文章较长请准备好瓜子板凳(liushuizhang😂)。
本文所有涉及的代码:
最终架构图:
本文接着上文应用限流进行讨论。
之前谈到的限流方案只能针对于单个 JVM 有效,也就是单机应用。而对于现在普遍的分布式应用也得有一个分布式限流的方案。
基于此尝试写了这个组件:
https://github.com/crossoverJie/distributed-redis-tool
以下采用的是
https://github.com/crossoverJie/springboot-cloud
来做演示。
在 Order 应用提供的接口中采取了限流。首先是配置了限流工具的 Bean:
1 | @Configuration |
接着在 Controller 使用组件:
1 | @Autowired |
为了方便使用,也提供了注解:
1 | @Override |
该注解拦截了 http 请求,会再请求达到阈值时直接返回。
普通方法也可使用:
1 | @CommonLimit |
会在调用达到阈值时抛出异常。
为了模拟并发,在 User 应用中开启了 10 个线程调用 Order(限流次数为5) 接口(也可使用专业的并发测试工具 JMeter 等)。
Python?Java?Ruby?JavaScript?有非常多的选择。选择一种编程语言开始你的编码之旅不应该是一件艰巨的任务。
事实上:你将要学习的语言并不是特别重要,更重要的是学习编程的理念。对于任何编程语言来说知识的可传递性都是至关重要的。
我学习的第一门语言是 Java,学习了循环,while 循环,条件,函数,面向对象编程和许多编程理念。
然而,选择一门能在编程领域轻松找到工作的语言是更好的选择。对于初学者来说,我这里有一份列表推荐给你:
LRU 是 Least Recently Used
的简写,字面意思则是最近最少使用
。
通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被撑满。
如常用的 Redis 就有以下几种策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
no-envicition | 禁止驱逐数据 |
分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三。
首先谈到分布式锁自然也就联想到分布式应用。
在我们将应用拆分为分布式应用之前的单机系统中,对一些并发场景读取公共资源时如扣库存,卖车票之类的需求可以简单的使用同步或者是加锁就可以实现。
但是应用分布式了之后系统由以前的单进程多线程的程序变为了多进程多线程,这时使用以上的解决方案明显就不够了。
因此业界常用的解决方案通常是借助于一个第三方组件并利用它自身的排他性来达到多进程的互斥。如:
NX EX
参数。这里主要基于 Redis 进行讨论。
不管是在面试还是实际开发中 volatile
都是一个应该掌握的技能。
首先来看看为什么会出现这个关键字。
由于 Java
内存模型(JMM
)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:
所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。
显然这肯定是会出问题的,因此 volatile
的作用出现了:
当一个变量被
volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
众所周知 HashMap 是一个无序的 Map
,因为每次根据 key
的 hashcode
映射到 Entry
数组上,所以遍历出来的顺序并不是写入的顺序。
因此 JDK 推出一个基于 HashMap
但具有顺序的 LinkedHashMap
来解决有排序需求的场景。
它的底层是继承于 HashMap
实现的,由一个双向链表所构成。
LinkedHashMap
的排序方式有两种:
其中根据访问顺序排序时,每次 get
都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表。
使用 synchronize
来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。
而 ReentrantLock
就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)
来实现的。
是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。
AQS
是Java
并发包里实现锁、同步的一个重要的基础框架。
ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型:
1 | //默认非公平锁 |
默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多(后面会分析具体原因)。