前言
之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的应用形式,然而当初很多利用零碎都是相当宏大的,很多利用零碎都是微服务的架构体系,那么在这种跨 jvm 的场景下,咱们又该如何去解决并发。
单体利用锁的局限性
在进入实战之前简略和大家粗略聊一下互联网零碎中的架构演进。
在互联网零碎倒退之初,耗费资源比拟小,用户量也比拟小,咱们只部署一个 tomcat 利用就能够满足需要。一个 tomcat 咱们能够看做是一个 jvm 的过程,当大量的申请并发达到零碎时,所有的申请都落在这惟一的一个 tomcat 上,如果某些申请办法是须要加锁的,比方上篇文章中提及的秒杀扣减库存的场景,是能够满足需要的。然而随着访问量的减少,一个 tomcat 难以撑持,这时候咱们就须要集群部署 tomcat,应用多个 tomcat 撑持起零碎。
在上图中简略演变之后,咱们部署两个 Tomcat 独特撑持零碎。当一个申请达到零碎的时候,首先会通过 nginx,由 nginx 作为负载平衡,它会依据本人的负载平衡配置策略将申请转发到其中的一个 tomcat 上。当大量的申请并发拜访的时候,两个 tomcat 独特承当所有的访问量。这之后咱们同样进行秒杀扣减库存的时候,应用单体利用锁,还能满足需要么?
之前咱们所加的锁是 JDK 提供的锁,这种锁在单个 jvm 下起作用,当存在两个或者多个的时候,大量并发申请扩散到不同 tomcat,在每个 tomcat 中都能够避免并发的产生,然而多个 tomcat 之间,每个 Tomcat 中取得锁这个申请,又产生了并发。从而扣减库存的问题仍旧存在。这就是单体利用锁的局限性。那咱们如果解决这个问题呢?接下来就要和大家分享分布式锁了。
分布式锁
什么是分布式锁?
那么什么是分布式锁呢,在说分布式锁之前咱们看到单体利用锁的特点就是在一个 jvm 进行无效,然而无奈逾越 jvm 以及过程。所以咱们就能够下一个不那么官网的定义,分布式锁就是能够逾越多个 jvm,逾越多个过程的锁,像这样的锁就是分布式锁。
设计思路
因为 tomcat 是 java 启动的,所以每个 tomcat 能够看成一个 jvm,jvm 外部的锁无奈逾越多个过程。所以咱们实现分布式锁,只能在这些 jvm 外去寻找,通过其余的组件来实现分布式锁。
上图两个 tomcat 通过第三方的组件实现跨 jvm,跨过程的分布式锁。这就是分布式锁的解决思路。
实现形式
那么目前有哪些第三方组件来实现呢?目前比拟风行的有以下几种:
- 数据库,通过数据库能够实现分布式锁,然而高并发的状况下对数据库的压力比拟大,所以很少应用。
- Redis,借助 redis 能够实现分布式锁,而且 redis 的 java 客户端品种很多,所以应用办法也不尽相同。
- Zookeeper,也能够实现分布式锁,同样 zk 也有很多 java 客户端,应用办法也不同。
针对上述实现形式,老猫还是通过具体的代码例子来一一演示。
基于数据库的分布式锁
思路:基于数据库乐观锁去实现分布式锁,用的次要是 select … for update。select … for update 是为了在查问的时候就对查问到的数据进行了加锁解决。当用户进行这种行为操作的时候,其余线程是禁止对这些数据进行批改或者删除操作,必须期待上个线程操作结束开释之后能力进行操作,从而达到了锁的成果。
实现:咱们还是基于电商中超卖的例子和大家分享代码。
咱们还是利用上次单体架构中的超卖的例子和大家分享,针对上次的代码进行革新,咱们新键一张表,叫做 distribute_lock,这张表的目标次要是为了提供数据库锁,咱们来看一下这张表的状况。
因为咱们这边模仿的是订单超卖的场景,所以在上图中咱们有一条订单的锁数据。
咱们将上一篇中的代码革新一下抽取出一个 controller 而后通过 postman 去申请调用,当然后盾是启动两个 jvm 进行操作,别离是 8080 端口以及 8081 端口。实现之后的代码如下:
/**
* @author kdaddy@163.com
* @date 2021/1/3 10:48
* @desc 公众号“程序员老猫”*/
@Service
@Slf4j
public class MySQLOrderService {
@Resource
private KdOrderMapper orderMapper;
@Resource
private KdOrderItemMapper orderItemMapper;
@Resource
private KdProductMapper productMapper;
@Resource
private DistributeLockMapper distributeLockMapper;
// 购买商品 id
private 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);
KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
// 商品以后库存
Integer currentCount = product.getCount();
log.info(Thread.currentThread().getName()+"库存数"+currentCount);
// 校验库存
if (purchaseProductNum > currentCount){throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无奈购买");
}
// 在数据库中实现减量操作
productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());
// 生成订单
... 次数省略,源代码能够到老猫的 github 下载:https://github.com/maoba/kd-distribute
return order.getId();}
}
SQL 的写法如下:
select
*
from distribute_lock
where 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 库存数 1
8081 的申请:
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 库存数 0
11: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]
很显著第二个申请因为没有库存,导致最终购买失败的状况,当然这个场景也是合乎咱们失常的业务场景的。最终咱们数据库的状况是这样的:
很显著,咱们到此数据库的库存和订单数量也都正确了。到此咱们基于数据库的分布式锁实战演示实现,上面咱们来演绎一下如果应用这种锁,有哪些长处以及毛病。
- 长处:简略不便、易于了解、易于操作。
- 毛病:并发量大的时候对数据库的压力会比拟大。
- 倡议:作为锁的数据库和业务数据库离开。
写在最初
对于上述数据库分布式锁,其实在咱们的日常开发中用的也是比拟少的。基于 redis 以及 zk 的锁倒是用的比拟多一些,原本老猫想把 redis 锁以及 zk 锁放在这一篇中一起分享掉,然而再写在同一篇下面的话,篇幅就显得过长了,因而本篇就和大家分享这一种分布式锁。源码大家能够在老猫的 github 中下载到。地址是:https://github.com/maoba/kd-d…,前面老猫会把 redis 锁以及 zk 锁都分享给大家,敬请期待,当然更多的干货分享,也欢送大家关注公众号“程序员老猫”。