【分布式锁的演化】分布式锁竟然还能用MySQL?

前言

以前的文章中经过电商场景中秒杀的例子和你们分享了单体架构中锁的使用方式,可是如今不少应用系统都是至关庞大的,不少应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,咱们又该如何去解决并发。java

单体应用锁的局限性

在进入实战以前简单和你们粗略聊一下互联网系统中的架构演进。mysql

架构简单演化

在互联网系统发展之初,消耗资源比较小,用户量也比较小,咱们只部署一个tomcat应用就能够知足需求。一个tomcat咱们能够看作是一个jvm的进程,当大量的请求并发到达系统时,全部的请求都落在这惟一的一个tomcat上,若是某些请求方法是须要加锁的,好比上篇文章中说起的秒杀扣减库存的场景,是能够知足需求的。可是随着访问量的增长,一个tomcat难以支撑,这时候咱们就须要集群部署tomcat,使用多个tomcat支撑起系统。nginx

在上图中简单演化以后,咱们部署两个Tomcat共同支撑系统。当一个请求到达系统的时候,首先会通过nginx,由nginx做为负载均衡,它会根据本身的负载均衡配置策略将请求转发到其中的一个tomcat上。当大量的请求并发访问的时候,两个tomcat共同承担全部的访问量。这以后咱们一样进行秒杀扣减库存的时候,使用单体应用锁,还能知足需求么?git

以前咱们所加的锁是JDK提供的锁,这种锁在单个jvm下起做用,当存在两个或者多个的时候,大量并发请求分散到不一样tomcat,在每一个tomcat中均可以防止并发的产生,可是多个tomcat之间,每一个Tomcat中得到锁这个请求,又产生了并发。从而扣减库存的问题依旧存在。这就是单体应用锁的局限性。那咱们若是解决这个问题呢?接下来就要和你们分享分布式锁了。程序员

分布式锁

什么是分布式锁?

那么什么是分布式锁呢,在说分布式锁以前咱们看到单体应用锁的特色就是在一个jvm进行有效,可是没法跨越jvm以及进程。因此咱们就能够下一个不那么官方的定义,分布式锁就是能够跨越多个jvm,跨越多个进程的锁,像这样的锁就是分布式锁。github

设计思路

分布式锁思路

因为tomcat是java启动的,因此每一个tomcat能够当作一个jvm,jvm内部的锁没法跨越多个进程。因此咱们实现分布式锁,只能在这些jvm外去寻找,经过其余的组件来实现分布式锁。redis

上图两个tomcat经过第三方的组件实现跨jvm,跨进程的分布式锁。这就是分布式锁的解决思路。sql

实现方式

那么目前有哪些第三方组件来实现呢?目前比较流行的有如下几种:数据库

  • 数据库,经过数据库能够实现分布式锁,可是高并发的状况下对数据库的压力比较大,因此不多使用。
  • Redis,借助redis能够实现分布式锁,并且redis的java客户端种类不少,因此使用方法也不尽相同。
  • Zookeeper,也能够实现分布式锁,一样zk也有不少java客户端,使用方法也不一样。

针对上述实现方式,老猫仍是经过具体的代码例子来一一演示。tomcat

基于数据库的分布式锁

思路:基于数据库悲观锁去实现分布式锁,用的主要是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-distribute,后面老猫会把redis锁以及zk锁都分享给你们,敬请期待,固然更多的干货分享,也欢迎你们关注公众号“程序员老猫”。

相关文章
相关标签/搜索