通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
一、搭建集群
1、我们将服务启动两份,端口分别为8081和8082,从而模拟集群效果:
首先,复制原来的user-service启动配置,或者 ctrl + D:
作用就是将新启动配置拷贝了一份,每创建一次新的启动配置,将来IDEA底层就会给我们创建两台tomcat,分别启动,就可以模拟两台机器了
![image-20240528074136124](https://img-blog.csdnimg.cn/img_convert/2a0c47160cb66daa7e5a39d307c8ba46.png)
![image-20240528074251969](https://img-blog.csdnimg.cn/img_convert/e5106894d6b3f5a6e85e15be3602c3ee.png)
然后,在弹出的窗口中,填写信息,由于将一个服务启动两次会有端口冲突,所以这里需要配置一个 -Dserver.prot
去避免端口冲突。
-D代表参数,server.port就是我们在yml文件中配的方式,这里配置的端口就是用来覆盖yml文件里的端口的。
![image-20240528074407829](https://img-blog.csdnimg.cn/img_convert/b78758fd26dc142ac53b572f14767f50.png)
最后重启两个服务
![image-20240528074631753](https://img-blog.csdnimg.cn/img_convert/63d37afa00667e6ba471a7d5c3cb819c.png)
现在我们就形成了一个集群了,它有两个节点的集群
二、负载均衡
2、然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
实现:当你访问Nginx的那一刻,它就会反向代理到8081/8082两个节点上,默认采用的就是轮询的负载均衡规则,这样它们两个都能访问到了。
然后通过 nginx.exe -s reload
重新加载Nginx的配置文件
![image-20240528075350191](https://img-blog.csdnimg.cn/img_convert/6bba1dd8f4eb9fae44ce36e76034edb6.png)
访问 http://localhost:8080/api/voucher/list/1
,8080端口不是8081,也不是8082,而现在访问的8080就是Nginx
![image-20240528075535730](https://img-blog.csdnimg.cn/img_convert/423a4f72be9bbba56663f2adcc52e78c.png)
Nginx监听到8080后,它的api路径就会代理到backed,backed就会负载均衡到8081和8082
![image-20240528075731480](https://img-blog.csdnimg.cn/img_convert/c3828c782ed975f5ce21f0b98582b86a.png)
所以我们访问8080其实就是在访问8081和8082节点。多访问几次 http://localhost:8080/api/voucher/list/1
,可以发现8081和8082都有请求,说明这两个节点都被访问到了,也就是说它们其实有了这样一个负载均衡的效果了
三、测试秒杀下单
在锁的地方打断点,并且将id为10的优惠券库存改为100,并且清除订单表中所有订单
利用Postman使用同一个用户的token发送请求
这里明明有锁,并且用户ID是一样的,就表示锁没锁住。两台服务都进到断点了就有问题了
如果此时放行,它们此时做查询,查到的结果都是count为0,如果此时放行,它俩都会去减库存,然后都创建订单。
此时去数据库看,可以发现库存扣了2个,并且有两个订单。
这就说明我们又一次出现了并发的安全问题。也就是在集群下,虽然我们使用了 synchronized锁
,但是并没有锁住。
四、有关锁失效原因分析
正常串行执行情况应该如下图
![image-20240528083500007](https://img-blog.csdnimg.cn/img_convert/7edaf122fe59f4ad326e89363a0c08d0.png)
现在在多线程并发执行的情况下,它不可能每次都这么正常的串行执行,它可能会出现交叉执行的情况,一旦出现交叉执行,这个订单就会被插入两次,如下图
![image-20240528083732355](https://img-blog.csdnimg.cn/img_convert/edff35134a17b0b4527750fbfa2265c9.png)
后来我们加了锁解决了这个问题,也就是说一个线程来了后必选先获取锁,拿到锁后才可以去执行查询订单的动作。
此时就锁另外一个线程要来获取锁,但由于线程1已经拿到了锁,因此线程2获取锁会失败。
根据synchronized的原理,它会等待,等待锁释放,然后就可以获取锁成功了,但此时再去执行查询,由于线程1已经插入了订单了,因此再来查已经存在了,此时订单就会报错。
![image-20240528085514865](https://img-blog.csdnimg.cn/img_convert/bfa89096105ee8fc2933aa4487350ec8.png)
现在我们不是一台这样,而是多台,大家知道,在一个JVM的内部,锁的原理是:在JVM的内部维护了一个锁的监视器对象,这个监视器对象我们用的是UserId,Id在常量池中。在这一个JVM的内部维护了一个池子,在Id相同的情况下,就永远是同一个锁,也就是锁的监视器是同一个。
因此无论是线程1也好,还是线程2也好,当线程1来获取锁的时候,锁监视器就会记录锁的名称,当形参2再来获取的时候,它一看这已经有了,它就不能获取了。
![image-20240528085911831](https://img-blog.csdnimg.cn/img_convert/40cf28cbc21bbc53c920de1c8cbc11d6.png)
但是当我们做集群部署的时候,一个新的部署,就相当于是一个全新的tomcat,也就意味着是一个全新的JVM,也就是说有两套JVM。
有两个JVM就有各自的堆、栈、方法区等,因此JVM2也会有自己的常量池,它在监视锁的时候,就会有一个全新的锁监视器了,跟JVM1的锁监视器不是同一个。
那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。