单体应用锁的局限性在进入实战之前简单和大家粗略聊一下互联网系统中的架构演进。
我看愣了,MySQL 还能实现分布式锁?在互联网系统发展之初,消耗资源比较小,用户量也比较小,我们只部署一个 tomcat 应用就可以满足需求。一个 tomcat 我们可以看做是一个 jvm 的进程,当大量的请求并发到达系统时,所有的请求都落在这唯一的一个 tomcat 上,如果某些请求方法是需要加锁的,比如上篇文章中提及的秒杀扣减库存的场景,是可以满足需求的。但是随着访问量的增加,一个 tomcat 难以支撑,这时候我们就需要集群部署 tomcat,使用多个 tomcat 支撑起系统。
在上图中简单演化之后,我们部署两个 Tomcat 共同支撑系统。当一个请求到达系统的时候,首先会经过 nginx,由 nginx 作为负载均衡,它会根据自己的负载均衡配置策略将请求转发到其中的一个 tomcat 上。当大量的请求并发访问的时候,两个 tomcat 共同承担所有的访问量。这之后我们同样进行秒杀扣减库存的时候,使用单体应用锁,还能满足需求么?
之前我们所加的锁是 JDK 提供的锁,这种锁在单个 jvm 下起作用,当存在两个或者多个的时候,大量并发请求分散到不同 tomcat,在每个 tomcat 中都可以防止并发的产生,但是多个 tomcat 之间,每个 Tomcat 中获得锁这个请求,又产生了并发。从而扣减库存的问题依旧存在。这就是单体应用锁的局限性。那我们如果解决这个问题呢?接下来就要和大家分享分布式锁了。
分布式锁什么是分布式锁?
那么什么是分布式锁呢,在说分布式锁之前我们看到单体应用锁的特点就是在一个 jvm 进行有效,但是无法跨越 jvm 以及进程。所以我们就可以下一个不那么官方的定义,分布式锁就是可以跨越多个 jvm,跨越多个进程的锁,像这样的锁就是分布式锁。
设计思路
我看愣了,MySQL 还能实现分布式锁?由于 tomcat 是 java 启动的,所以每个 tomcat 可以看成一个 jvm,jvm 内部的锁无法跨越多个进程。所以我们实现分布式锁,只能在这些 jvm 外去寻找,通过其他的组件来实现分布式锁。
上图两个 tomcat 通过第三方的组件实现跨 jvm,跨进程的分布式锁。这就是分布式锁的解决思路。
实现方式
那么目前有哪些第三方组件来实现呢?目前比较流行的有以下几种:
数据库,通过数据库可以实现分布式锁,但是高并发的情况下对数据库的压力比较大,所以很少使用。Redis,借助 redis 可以实现分布式锁,而且 redis 的 java 客户端种类很多,所以使用方法也不尽相同。Zookeeper,也可以实现分布式锁,同样 zk 也有很多 java 客户端,使用方法也不同。针对上述实现方式,还是通过具体的代码例子来一一演示。
基于数据库的分布式锁思路:基于数据库悲观锁去实现分布式锁,用的主要是 select … for update。select … for update 是为了在查询的时候就对查询到的数据进行了加锁处理。当用户进行这种行为操作的时候,其他线程是禁止对这些数据进行修改或者删除操作,必须等待上个线程操作完毕释放之后才能进行操作,从而达到了锁的效果。
实现:我们还是基于电商中超卖的例子和大家分享代码。
咱们还是利用上次单体架构中的超卖的例子和大家分享,针对上次的代码进行改造,我们新键一张表,叫做 distribute_lock,这张表的目的主要是为了提供数据库锁,我们来看一下这张表的情况。
我看愣了,MySQL 还能实现分布式锁?由于我们这边模拟的是订单超卖的场景,所以在上图中我们有一条订单的锁数据。
我们将上一篇中的代码改造一下抽取出一个 controller 然后通过 postman 去请求调用,当然后台是启动两个 jvm 进行操作,分别是 8080 端口以及 8081 端口。完成之后的代码如下:
/**
-
@author kdaddy@163.com
-
@date 2021/1/3 10:48
-
@desc /@Service@Slf4jpublic class MySQLOrderService {@Resourceprivate KdOrderMapper orderMapper;@Resourceprivate KdOrderItemMapper orderItemMapper;@Resourceprivate KdProductMapper productMapper;@Resourceprivate DistributeLockMapper distributeLockMapper;//购买商品 idprivate int purchaseProductId = 100100;//购买商品数量 private int purchaseProductNum = 1;
-
@Transactional(propagation = Propagation.REQUIRED)public Integer createOrder() throws Exception{log.info(“进入了方法”);DistributeLock lock = distributeLockMapper.selectDistributeLock(“order”);if(lock == null) throw new Exception(“该业务分布式锁未配置”);log.info(“拿到了锁”);//此处为了手动演示并发,所以我们暂时在这里休眠 1 分钟 Thread.sleep(60000);
-
}}SQL 的写法如下:
select*from distribute_lockwhere business_code = #{business_code,jdbcType=VARCHAR}for update 以上为主要实现逻辑,关于代码中的注意点:
createOrder 方法必须要有事务,因为只有在事务存在的情况下才能触发 select for update 的锁。代码中必须要对当前锁的存在性进行判断,如果为空的情况下,会报异常我们来看一下最终运行的效果,先看一下 console 日志,
8080 的 console 日志情况:
11:49:41 INFO 16360 — [nio-8080-exec-2] c.k.d.service.MySQLOrderService : 进入了方法 11:49:41 INFO 16360 — [nio-8080-exec-2] c.k.d.service.MySQLOrderService : 拿到了锁 8081 的 console 日志情况:
11:49:48 INFO 17640 — [nio-8081-exec-2] c.k.d.service.MySQLOrderService : 进入了方法通过日志情况,两个不同的 jvm,由于第一个到 8080 的请求优先拿到了锁,所以 8081 的请求就处于等待锁释放才会去执行,这说明我们的分布式锁生效了。
再看一下完整执行之后的日志情况:
8080 的请求:
11:58:01 INFO 15380 — [nio-8080-exec-1] c.k.d.service.MySQLOrderService : 进入了方法 11:58:01 INFO 15380 — [nio-8080-exec-1] c.k.d.service.MySQLOrderService : 拿到了锁 11:58:07 INFO 15380 — [nio-8080-exec-1] c.k.d.service.MySQLOrderService : http-nio-8080-exec-1 库存数 18081 的请求:
11:58:03 INFO 16276 — [nio-8081-exec-1] c.k.d.service.MySQLOrderService : 进入了方法 11:58:08 INFO 16276 — [nio-8081-exec-1] c.k.d.service.MySQLOrderService : 拿到了锁 11:58:14 INFO 16276 — [nio-8081-exec-1] c.k.d.service.MySQLOrderService : http-nio-8081-exec-1 库存数 011:58:14 ERROR 16276 — [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.Exception: 商品 100100 仅剩 0 件,无法购买] with root cause
java.lang.Exception: 商品 100100 仅剩 0 件,无法购买 at com.kd.distribute.service.MySQLOrderService.createOrder(MySQLOrderService.java:61) ~[classes/:na]很明显第二个请求由于没有库存,导致最终购买失败的情况,当然这个场景也是符合我们正常的业务场景的。最终我们数据库的情况是这样的:
我看愣了,MySQL 还能实现分布式锁?我看愣了,MySQL 还能实现分布式锁?很明显,我们到此数据库的库存和订单数量也都正确了。到此我们基于数据库的分布式锁实战演示完成,下面我们来归纳一下如果使用这种锁,有哪些优点以及缺点。
优点:简单方便、易于理解、易于操作。缺点:并发量大的时候对数据库的压力会比较大。建议:作为锁的数据库和业务数据库分开。写在最后对于上述数据库分布式锁,其实在我们的日常开发中用的也是比较少的。基于 redis 以及 zk 的锁倒是用的比较多一些,本来老猫想把 redis 锁以及 zk 锁放在这一篇中一起分享掉,但是再写在同一篇上面的话,篇幅就显得过长了,因此本篇就和大家分享这一种分布式锁。
小伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。
原创:程序员老猫
原文出处:xie.infoq.cn/article/2fec0f6755d537c7c165d8347