分布式篇(分布式锁 - 史上最全分布式锁)(持续更新调整)

目录

一、简介

二、分布式锁的简介

三、传统的锁机制

四、什么是共享资源

五、锁应用的场景

六、JDK提供的锁

七、单体架构

1. 案例一:银行存钱和取钱的并发问题

1.1. 创建一个springboot工程

1.2. 定义LockOne和LockThread

1.3. Java实现多线程

无锁版本

Synchronized锁版本

2. 案例二:火车站的售票窗口抢票

2.1. synchronized版本

2.2. lock锁版本

2.3 知识小结

八、分布式锁登场

1. 何为分布式锁呢?

2. 分布式锁需要具备那些条件

3. 常见的分布式锁的解决方案有

4. 分布式锁的应用场景

4.1. 重复提交

4.2. 商城高并发秒杀或者抢购

5. 小结

九、基于数据库的方式实现分布式锁 - 乐观锁

1. 乐观锁简介

2. 乐观锁实战 - 余额体现

2.1. 新建一个项目springboot

依赖

配置文件

注解

2.2. 新建一个用户余额表和用户提现记录表

2.3. 创建对应文件

entity

mapper

mapper.xml

service

controller

jmeter压力测试看结果

新建线程组

添加并发请求的接口

十、基于数据库的方式实现分布式锁 - 悲观锁

1. 悲观锁简介

2. 具体实现

3. 实战开发

4. 小结

5. 上锁目的

十一、基于Zookeeper的方式实现分布式锁 - 商品抢购

1. 简介

2. Zookeeper可以用来干嘛?

3. Zookeeper分布式锁流程

4. SpringBoot整合Zookeeper

4.1. 依赖

4.2. 配置

4.3. 配置初始化

4.4. 用户注册实现分布式锁

5. 实现商品抢购扣减库存

十二、基于Zookeeper 完成 实现商品抢购扣减库存

1. 案例场景

2. 流程架构

3. 表设计

4. 具体实现

依赖

配置

配置初始化

bean

mapper

service

serviceimpl

productvo

controller

十二、基于Redisson的方式实现分布式锁 -秒杀

十三、基于Redisson的方式实现分布式锁 -一次性锁实战

1. 简介

2. SpringBoot整合Redisson

依赖

初始化Redisson的客户端连接

使用redisson的功能–用户注册

3. Redisson的分布式锁

4. Redisson的一次性锁实战

5. Redisson分布式锁之可重入实战

十四、基于Redisson的方式的生产者与消费者模式-完成消息的发送和消费

1. 简介

2. Publish / Subscribe 发布和订阅

3. 具体实现

依赖

配置文件

配置类

生产者发送消息

消费者接收消息

十五、基于Redis的方式分布式锁 - 用户注册

1. 简介

2. Redis的典型应用场景

3. Redis实现分布式锁 - 幂等性问题

4. 图解

5. 实现用户注册

5.1. 依赖

5.2. 配置

5.3. sql脚本

5.4. 配置类

5.5. bean,mapper

5.6. 实现分布式锁

5.7. controller

6. 小结

十六、基于Zookeeper的方式实现分布式锁 - 用户注册

1. 简介

2. Zookeeper可以用来干嘛?

3. Zookeeper分布式锁流程

4. SpringBoot整合Zookeeper

依赖

配置

配置初始化

用户注册实现分布式锁

十七、微服务中分布式锁实现与原理分析

1. 多线程并发引发数据超卖现象

目标

场景分析

案例代码

测试用例

结果

小结

面试题:线程的 A,B,C模式

2. 解决多线程并发引发数据超卖现象

目标

分析

理解

代码

测试

小结

3. 解决多线程并发引发数据超卖现象 - lock

目标

分析

代码

测试

小结

4. 分布式锁级实现方案和特点

何谓分布式锁

分布式锁架构

分布式锁需要具备哪些条件

应用场景

分布式的几个话题

实现分布式锁的几种方案

小结

5. 分布式锁Redisson

5.1. 何为分布式锁Redisson

5.2. 实现步骤

导入依赖

初始化RLock

业务执行

测试类

5.3 小结

6. 自定义分布式锁Redis

目标

分析

步骤

代码

导入依赖

初始化jedis及配置信息

解析properties的工具类

分布式锁类

定义业务执行类

测试用例

小结

7. 商品抢购结合分布式锁案例分析

目标

代码

springboot创建一个ssm工程

初始化数据库脚本

相关依赖包的引入

配置连接信息

jdbctemplate完成对商品的库存查询和扣减定义和实现

定义redis.properties和解析类

定义redis的工具类,连接redis

分布式锁实现

使用分布式锁解决超卖问题

测试用例

小结

8. 利用Redis生成业务流水号思路(分布式订单编号)

目标

测试

9. Redis缓存击穿的问题?

10. 基于Redis实现分布式红锁

目标

分析

红锁算法的设计原理

部署Redis的3台机器部署

基于配置文件方式

步骤1:把配置文件复制三份

步骤2:然后启动对应配置

步骤3:如果是阿里云服务器或者自己本机安装虚拟机

基于docker的方式进行安装

步骤1:安装三台redis的容器服务

基于三台的redis的分布式红锁

步骤1:配置三台服务配置

步骤2:初始化三个redisson服务对象

步骤3:进行分分布式红锁的控制

十五、知识小结


一、简介

集群下个定义:有多个物理节点组合而成,共同协作完成一件事。(具备:高并发和高可用和高可扩)

在互联网,移动互联网的时代,企业应用系统大多采用集群,分布式的方式进行部署,将“业务高度集中”的传

统企业级应用按照业务拆分成多个子系统,并进行独立部署,而为了应对某些业务场景下产生的高并发请求,通

常一个子系统会部署多分实例,并采用某个均衡机制“分摊”处理前端用户的请求,此种方式俗称:“集群”。

事实证明,此种分布式,集群部署的方式确实功能给企业级的系统应用带来性能和效率的提升,从而给企业业务

规模带来课扩展的可能性。

然而,任何事务都并非十全十美,正如服务集群,分布式系统架构一样,虽然可以给企业应用系统带来性能,质

量和效率上的提升,但也由此带来了一些棘手的问题,其中比如典型的问题就是:==高并发场景下多个线程并发

访问,造成共享资源泄露的问题。会造成数据的不一致现象。针对这个问题,业界普遍的采取的方式是用:“分

布式锁”,加以解决。

本次实践:主要针对分布式锁,出现的背景,以及使用场景进行说明和实战。

二、分布式锁的简介

在传统的单体架构的时代,“并发访问,操作共享资源“的场景并不少见,由于那个时期还没有:”分布式“的

概念,故而当多个线程并发访问,操作共享资源时,往往通过加同步互斥锁进行控制,这种方式在很长的一段时

间内确实能起到一定的作用。

但是随着用户,数据量的增长,企业为了使用,不得不对单一的应用系统进行拆分并作分布式部署。

而这个时候分布式系统架构部署方案的实现带来性能和效率上的提升的同时,也带来了一些问题,即传统枷锁的

方式将不再器作用。这是因为集群,分布式部署的服务实列一般是部署在不同服务器上的。在分布式系统架构

下,此种资源共享将不再是传统的线程共享,而是跨JVM进程的资源共享,因此为了解决这个问题,我们引入

了:“分布式锁”进行控制。

三、传统的锁机制

在单体应用时代,传统企业应用未了解决资源共享造成数据不一致问题,通常的解决方案是利用JDK自身提供的锁

关键字或者JUC并发工具类,如:Synchronized、Lock、RetreenLock等加以实现,这种访问控制机制被业界普

通认为:“锁”,不可否认的是,在很长一段时间内确实起到了作用,==但是在分布式和集群环境下就会失去其

意义和价值。

四、什么是共享资源

共享资源是指:可以被多个线程,进程同时访问进行操作的数据或者代码块。

比如春运期间抢购的火车票,在电商平台抢购的商品,秒杀活动的商品等都属于典型可供多个用户获取、操作甚

至共享的东西,这些东西称之为:“共享资源”。

  • 更新操作,这个时候很久特别的注意。

我怎么判别会造成资源泄露:

-  select 
-  insert —- 重复提交,幂等性问题----锁 --- 重复提交
-  update — 可能就引发幂等性和超卖问题-- 锁---超卖问题 —-

幂等性是指:在实际开发和操作中,你执行和处理一个业务无论你执行多次(n次)你产生的结果应该和预期的是一

致。

那么久你业务就遵循幂等性。一句话:不论你执行的多次你最终的结果都是一样的。

  • 举例子:用户注册----用户不论疯狂点击还是用程序轮询攻击,在那一时刻不论用户一个用户就只能有一条,
  • 举例子:用户下单,----用户不论疯狂点击还是用程序轮询攻击,在那一时刻不论用户一个用户就只能有一个

订单。

五、锁应用的场景

  • 重复提交
    • 银行存储和取钱
    • 用户注册
    • 网银体现
    • 微信支付体现
    • 抢红包
    • 抢优惠券
  • 超卖问题
    • 下单
    • 抢购
    • 秒杀
    • 抢红包

六、JDK提供的锁

  • Synchronized 和 Lock ,仅限于单体架构,如果你的项目没有集群部署可以使用,
  • 如果你项目是集群部署。这种jdk自带的锁就失去意义和价值。
  • 你只能通过:数据库乐观锁,悲观锁,redis的锁,zookeeper锁,redssion的锁解决。

七、单体架构

1. 案例一:银行存钱和取钱的并发问题

“银行ATM存钱和取钱” ,比如:怪咖在户口初始余额是:500元,用A和用户B同时分别在不同的ATM机上进行

银行账号10次的存钱和取钱操作,其中A用户每次的操作都是存入100元,而B每次的操作都是取出100元,从理

论上讲,这个银行账号不管经过多少次:

存入和取出100元”操作,该账户的余额始终都是:500才对。

在该业务中,共享资源即为:”银行账户的余额“,而不同线程发起的共享访问操作包括:”存钱入银行账户“

和”从该银行账户取钱“。

想象大部分都是美好的,而现实是骨感和丰满的。在程序中如果用多线程并发操作的时候,如果不进行处理,可

能资源的不一致,从而银行或者客户都会带来”灾难性“的结果,比如:银行资金流失,客户总账不对等问题。

接下来我们用带来来实现这个问题:

1.1. 创建一个springboot工程

1.2. 定义LockOne和LockThread

1.3. Java实现多线程

  • 无返回值实现接口Runnable
  • 继承类Thread (不推荐,java是多实现,单一继承)
  • 有返回值:Callabled
无锁版本
//模拟锁机制的线程类
class LockThread implements Runnable {
    private static final Logger log = LoggerFactory.getLogger(LockThread.class);

    //定义成员变量-用于接收线程初始化时提供的金额-代表取/存的金额
    private int count;

    //构造方法
    public LockThread(int count) {
        this.count = count;
    }

    /**
     * 线程操作共享资源的方法体-不加同步锁
     */
    @Override
    public void run() {
        try {
            //执行10次访问共享的操作
            for (int i = 0; i < 10; i++) {
                //通过传进来的金额(可正、可负)执行叠加操作
                SysConstant.amount = SysConstant.amount + count;
                //打印每次操作完账户的余额
                log.info("此时账户余额为:{}", SysConstant.amount);
            }
        } catch (Exception e) {
            //有异常情况时直接进行打印
            e.printStackTrace();
        }
    }
}


public class LockOne {
    private static final Logger log = LoggerFactory.getLogger(LockOne.class);

    public static void main(String args[]) {
        // 存钱
        Thread tAdd = new Thread(new LockThread(100));
        // 取钱
        Thread tSub = new Thread(new LockThread(-100));
        tAdd.start();
        tSub.start();
    }
}

结果

出现不一致情况,因为此时我们并没有加:“同步访问呢”的控制机制,导致:“账户余额”这一共享资源最终

出现了数据不一致情况。

可能会多,可能会少。

在传统的单体架构应用时代,针对并发访问共享资源出现数据不一致,即并发安全的问题的时候,一般都是使用

Synchronized或者Lock关键字来解决。

如下:

Synchronized锁版本
package com.zheng.travel.lock.jdk; /**
 * Created by Administrator on 2019/4/14.
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 锁机制
 **/
public class LockOne {
    private static final Logger log = LoggerFactory.getLogger(LockOne.class);

    public static void main(String args[]) {
        Thread tAdd = new Thread(new LockThread(100));
        Thread tSub = new Thread(new LockThread(-100));
        tAdd.start();
        tSub.start();
    }
}

//模拟锁机制的线程类
class LockThread implements Runnable {
    private static final Logger log = LoggerFactory.getLogger(LockThread.class);

    //定义成员变量-用于接收线程初始化时提供的金额-代表取/存的金额
    private int count;

    //构造方法
    public LockThread(int count) {
        this.count = count;
    }

    /**
     * 线程操作共享资源的方法体-加同步锁
     */
    @Override
    public void run() {
        //执行10次访问共享的操作
        for (int i = 0; i < 100; i++) {
            //加入 synchronized 关键字,控制并发线程对共享资源的访问
            synchronized (SysConstant.amount) {
                //通过传进来的金额(可正、可负)进行叠加
                SysConstant.amount = SysConstant.amount + count;
                //打印每次操作完账户的余额
                log.info("此时账户余额为:{}", SysConstant.amount);
            }
        }
    }
}

不论你执行多少次,都不会存在资源和数据的不一致情况。如下:

都属于正常的状况。当然采用Synchronized关键词实现同步锁的方式,在实际生产环境中仍然是又一些缺陷的。

2. 案例二:火车站的售票窗口抢票

比如:火车站的售票窗口还有100张票,这个时候三个黄牛同时在抢票,或者N个用户在抢票。

看会发生什么问题?

package com.mzy.lock;

public class SellTicket implements Runnable{

    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            if(ticket > 0 ) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
            }
        }
    }
    
}

测试用例:

package com.zheng.lock2;

public class SellTicketTest  {


    public static void main(String[] args){

        // 买票测试
        SellTicket sellTicket = new SellTicket();
        for (int i = 1  ; i <=3 ; i++) {
            new Thread(sellTicket,"窗口-"+i).start();
        }
    }

}

运行结果如下:

Thread-0正在出售第:100张票!
Thread-1正在出售第:99张票!
Thread-2正在出售第:98张票!
Thread-2正在出售第:97张票!
Thread-0正在出售第:95张票!
Thread-1正在出售第:96张票!
Thread-2正在出售第:94张票!
Thread-1正在出售第:93张票!
Thread-0正在出售第:92张票!
Thread-2正在出售第:91张票!
Thread-1正在出售第:90张票!
Thread-0正在出售第:90张票!
Thread-2正在出售第:89张票!
Thread-0正在出售第:88张票!
Thread-1正在出售第:89张票!
Thread-2正在出售第:87张票!
Thread-1正在出售第:85张票!
Thread-0正在出售第:86张票!
Thread-1正在出售第:84张票!
Thread-0正在出售第:83张票!
Thread-2正在出售第:84张票!
Thread-2正在出售第:82张票!
Thread-1正在出售第:81张票!
Thread-0正在出售第:82张票!
Thread-1正在出售第:80张票!
Thread-2正在出售第:80张票!
Thread-0正在出售第:80张票!
Thread-1正在出售第:79张票!
Thread-0正在出售第:78张票!
Thread-2正在出售第:79张票!
Thread-1正在出售第:77张票!
Thread-0正在出售第:76张票!
Thread-2正在出售第:76张票!
Thread-2正在出售第:75张票!
Thread-0正在出售第:74张票!
Thread-1正在出售第:73张票!
Thread-2正在出售第:72张票!
Thread-0正在出售第:72张票!
Thread-1正在出售第:72张票!
Thread-2正在出售第:71张票!
Thread-0正在出售第:71张票!
Thread-1正在出售第:71张票!
Thread-2正在出售第:70张票!
Thread-0正在出售第:70张票!
Thread-1正在出售第:70张票!
Thread-1正在出售第:69张票!
Thread-0正在出售第:68张票!
Thread-2正在出售第:67张票!
Thread-1正在出售第:66张票!
Thread-0正在出售第:65张票!
Thread-2正在出售第:65张票!
Thread-2正在出售第:64张票!
Thread-1正在出售第:63张票!
Thread-0正在出售第:63张票!
Thread-2正在出售第:62张票!
Thread-1正在出售第:62张票!
Thread-0正在出售第:62张票!
Thread-2正在出售第:61张票!
Thread-0正在出售第:60张票!
Thread-1正在出售第:59张票!
Thread-0正在出售第:58张票!
Thread-2正在出售第:56张票!
Thread-1正在出售第:57张票!
Thread-0正在出售第:55张票!
Thread-2正在出售第:55张票!
Thread-1正在出售第:55张票!
Thread-1正在出售第:54张票!
Thread-0正在出售第:52张票!
Thread-2正在出售第:53张票!
Thread-2正在出售第:51张票!
Thread-1正在出售第:50张票!
Thread-0正在出售第:50张票!
Thread-0正在出售第:49张票!
Thread-2正在出售第:48张票!
Thread-1正在出售第:48张票!
Thread-0正在出售第:47张票!
Thread-2正在出售第:45张票!
Thread-1正在出售第:46张票!
Thread-1正在出售第:44张票!
Thread-0正在出售第:43张票!
Thread-2正在出售第:43张票!
Thread-2正在出售第:42张票!
Thread-0正在出售第:41张票!
Thread-1正在出售第:41张票!
Thread-1正在出售第:40张票!
Thread-2正在出售第:39张票!
Thread-0正在出售第:38张票!
Thread-2正在出售第:37张票!
Thread-1正在出售第:37张票!
Thread-0正在出售第:37张票!
Thread-2正在出售第:36张票!
Thread-1正在出售第:36张票!
Thread-0正在出售第:36张票!
Thread-1正在出售第:35张票!
Thread-0正在出售第:34张票!
Thread-2正在出售第:34张票!
Thread-1正在出售第:33张票!
Thread-0正在出售第:31张票!
Thread-2正在出售第:32张票!
Thread-0正在出售第:30张票!
Thread-1正在出售第:28张票!
Thread-2正在出售第:29张票!
Thread-1正在出售第:27张票!
Thread-2正在出售第:25张票!
Thread-0正在出售第:26张票!
Thread-1正在出售第:24张票!
Thread-2正在出售第:23张票!
Thread-0正在出售第:23张票!
Thread-2正在出售第:22张票!
Thread-0正在出售第:21张票!
Thread-1正在出售第:22张票!
Thread-0正在出售第:20张票!
Thread-2正在出售第:18张票!
Thread-1正在出售第:19张票!
Thread-0正在出售第:17张票!
Thread-1正在出售第:16张票!
Thread-2正在出售第:16张票!
Thread-2正在出售第:15张票!
Thread-1正在出售第:14张票!
Thread-0正在出售第:14张票!
Thread-1正在出售第:13张票!
Thread-0正在出售第:11张票!
Thread-2正在出售第:12张票!
Thread-0正在出售第:10张票!
Thread-2正在出售第:8张票!
Thread-1正在出售第:9张票!
Thread-1正在出售第:7张票!
Thread-0正在出售第:6张票!
Thread-2正在出售第:6张票!
Thread-0正在出售第:5张票!
Thread-1正在出售第:5张票!
Thread-2正在出售第:5张票!
Thread-0正在出售第:4张票!
Thread-1正在出售第:2张票!
Thread-2正在出售第:3张票!
Thread-1正在出售第:1张票!
Thread-0正在出售第:-1张票!
Thread-2正在出售第:0张票!

Process finished with exit code 0

出现了超买的的现象,如果解决这个问题呢?用锁来解决 线程具有重入性(执行完毕以后才可以继续争抢cpu资

源继续处理),无序性(谁先抢到CPU就谁执行),不会阻塞。

2.1. synchronized版本

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方

法或者访问synchronized代码快时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待

这个方法执行完毕或者代码快执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码

块。

package com.mzy.lock;

/**
 * 
 * 模拟多线程环境下资源竞争的问题
 * 
 */
public class SellTicket_Sync implements Runnable{
    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            synchronized (this) {
                if(ticket > 0 ) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                }
            }
        }
    }
}

测试

package com.mzy.lock;

public class SellTicketTest {

    public static void main(String[] args) {

        //使用Synchronized锁来解决线程安全问题		SellTicket_Sync sellTicket = new SellTicket_Sync();
        
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

结果

小结

使用 synchronized 的方式解决商品的超卖问题,系统级别的锁,只能jvm去维护,如果你单一的synchronized

修饰方法和代码块的时候是不会出现死锁的问题。并且它无法去控制和销毁完全都是jvm去控制有性能的损耗排他

性(互斥性 ),重入性。

2.2. lock锁版本

掌握lock的使用方法和存在的问题、java.util.concurrent.简称: J.U.C

Lock(轻量级)和synchronized同步块一样,是一种线程同步机制,但比java的synchronized同步块更复杂。

自Java5开始,在java.util.concurrent.locks包中包含了一些锁的实现,它们可以帮助我们解决进程内多线程并发

时的数据一致性问题。

Lock是一个接口,里面的方法有:

  • lock() 获取锁,如果锁被占用,其他的线程全部等待。(其他的线程全部阻塞,排它性
try{
  // A, B,C
  lock.lock(); //排他
   //写业务
   
}catch(ex){

} finally{
   lock.unLock()
}
  • tryLock() 如果获取锁的时候,锁被占用就返回false,否则返回true

(获取到锁的就执行,获取不到的就等待)

try{
   // A, B,C
   boolean flag = lock.tryLock();// b == true //排他
   if(flag){
      // 也业务
      
   }else{
          return "非常抱歉,你请稍后再试一试";
   }
   
}catch(ex){

} finally{
   lock.unLock()
}
  • tryLock(long time,TimeUnit unit) 如果获取锁的时候,锁被占用就返回false,否则返回true 并且释放时间
  • unLock() :释放锁(如果不锁就会出现死锁)
  • lockInterruptibly(); 用该锁的获得方式,如果线程在获取锁的阶段进入等待,那么可以中断次线程,先去做

别的事情。

package com.mzy.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * 模拟多线程环境下资源竞争的问题
 * 
 */
public class SellTicket_Lock implements Runnable{
    
    //定义锁
    private Lock lock = new ReentrantLock();
    //票数
    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            lock.lock();//加锁
            try {
                if(ticket > 0 ) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                }
                
            } finally {
                lock.unlock();//一定要释放锁
            }
        }
    }
}

测试

package com.mzy.lock;

public class SellTicketTest {

    public static void main(String[] args) {
        SellTicket_Lock  sellTicket = new SellTicket_Lock();
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

结果也不会超卖

2.3 知识小结

到底哪个好?

  • 两者在某种程度上来来说,没有太大的区别了。在后续oracle公司已经对synchronized进行优化了,性能几

乎和lock差不多

  • 所以不用纠结到底用lock还是sync,但是推荐大家使用:Lock
  • 自己命运能够自己掌握,就绝不交给别人,所以使用Lock

synchronized和lock的区别:

  • Lock是接口,而synchronized是Java的关键字
  • synchronized不会导致死锁现象发生,而Lock可能会造成死锁现象
  • Lock可以让等待锁的线程中断,而synchronized却不行
  • 通过Lock可以指定又没有成功获取锁,而synchronized却无法办到
  • Lock可以提高多线程进行读操作的效率
  • 在性能上来说,如果竞争不激烈,两者的性能是差不多的,而当竞争资源非常激烈的时候,此时Lock的性能

要远远优于synchronized,所以说在使用时,要根据适当情况选择。

八、分布式锁登场

在分布式系统中,常常 需要去协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一

组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到

分布式锁。

然而,不管是采用Synchronized关键字还是Lock的方式控制”共享资源“的访问,终归只适合单体应用或者单一

部署的服务实例。

而对分布式部署的系统或者集群部署的服务实例,此种方式将显得有点力不从心。

这是因为这种方式的:”锁“很大程度上需要依赖应用系统所在的JDK,比如Synchronized,Lock都是Java提供

给开发者的。

而在分布式系统时代,许多服务实例或者系统是分开部署的,他们将拥有自己独立的主机Host,独立的JDK,导

致应用系统在分布式部署的情况下,这种控制:“并发线程访问共享资源”的机制将不在起作用。故而使

用:“分布式锁”来解决这个问题。

1. 何为分布式锁呢?

分布式锁也是一种锁机制,只不过专门应对分布式环境而出现的,它并不是一种全新的中间件或者组件,而是一

种机制,一种实现方式,甚至可以说是一种解决方案。它指的是在分布式部署的环境下,通过锁机制让多个客户

端或者多个服务进程互斥地对共享资源进行访问,从而避免出现并发安全,数据不一致等问题。

或者这样理解

分布式锁:是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。

如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互

斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式锁是:控制分布式系统之间同步访问共享资源的一种方式

2. 分布式锁需要具备那些条件

  • 互斥性(排它性):和我们本地锁一样互斥性是最基本的。和单体应用时代是一个道理,即需要保证在分布

式部署,服务集群部署的环境下,被共享的资源(数据或者代码块)==在同一时刻只能被一台服务器上的线

程访问。其他的都被等待。

  • 避免死锁:指的是当前线程获取锁之后,经过一段有限的时间(该时间一般用于执行实际的业务逻辑),一

定要释放锁(正常情况和异常情况都要释放锁)。

  • 高可用性:高可用是指获取和释放锁的机制必须高可用和集群且性能极佳。
  • 可重入性:是指该分布式锁最好是一把可重入锁,即当前机器的当前线程在彼时如果没有获取到锁,那么在

等待一定的时间后要保证可用获取该锁。

  • 公平锁(可选):这个非硬性要求,指的是不同服务器的不同线程获取锁的概率最好保证是一样的。即应当保证

来自不同服务器的并发线程可用公平获取锁。

3. 常见的分布式锁的解决方案有

  • 基于数据库级别的
    • 乐观锁
      • 基于数据库级别的乐观锁,主要是通过在查询,操作共享数据记录时带上一个标识字段(version),

通过version来控制每次对数据记录执行的更新操作。

    • 悲观锁
      • 基于数据级别的悲观锁,这里以MYSQL的innodb为例,它主要通过在访问共享的数据记录时加上

for update 关键词,表示该共享的数据记录已经被当前线程锁住了,(行级锁,表级别锁,间隙

锁),只有当该线程操作完成并提交事务之后,才会释放该锁,从而其他线程才能访问该数据记

录。

  • 基于Redis的原子操作
    • setnx +expire原子操作 (时间问题)
      • 主要是通过Redis提供的原子操作setnx+ expire来实现,setnx表示只有当key在redis不存在时才能

设置成功,通过这个key需要设置为与共享的资源有联系,用于间接地当做锁,并采用expire操作释

放获取的锁。

  • 基于Zookeeper的互斥排它锁
    • 主要通过创建有序节点 +Watch机制
      • 主要是通过zk指定的标识字符串(通常这个标识字符串需要设置为与共享资源有联系,即可以间接

地当作:“锁”)下维护一个临时有序的节点列表NodeList,并保证同一时刻并发线程访问共享资

源是只能有一个最小序号的节点,(即代表获取到锁的线程)。该节点对应的线程即可执行访问共

享资源的操作。

  • 基于Redisson的分布式锁
    • 推荐
    • 时间狗

4. 分布式锁的应用场景

  • 重复提交
    • 资源冗余
  • 超卖资源泄露
    • 商城高并发抢购
    • 商城高并发秒杀
    • 抢红包
    • 抽奖

这些场景都有共同的特征就是:资源少,请求大,除了用分不锁来解决以外,

还必须加:限流才能够稳定的处理。

4.1. 重复提交

在实际开发中我们经常开发注册功能,大概流程是这样:

  • 用户在前端输入相关的用户信息(比如用户名,密码等)之后,
  • 疯狂地点击“注册”按钮,
  • 此时前端可能会做一些 “按钮置灰”,节流、防抖等功能,
  • 但是仍然存在有一些不可避免的不可控因素,导致前端提交了多次重复的,
  • 相同的用户信息到系统后端,系统后端后端相关接口在执行:“查询用户名是否存在” 和 “将用户信息插入

到数据库”,

由于“来不及” 处理线程并发的情况,最终导致在用户的数据库表中,存在2条甚至多条相同的用户信息记录。

这个时候,不难发现:“查询用户名是否存在”的操作存在问题,当多个线程比如:A,B,C同时到达后端接口时,

很有可能同时执行”查询用户名是否存在“ 的操作,而由于用户是首次注册,用户数据此时还没有数据记录。故

而A,B,C三个线程很有可能同时得到:”查询用户名是否存在“的结果,导致3个线程同时执行了:”将用户信息

插入数据库“的操作,最终导致数据重复出现的现象。因此我们可以使用:“分布式锁” 来进行解决这个问题。

4.2. 商城高并发秒杀或者抢购

对于大型的:“商城系统高并发的秒杀和抢购”的业务场景,是非常常见的,在前面我们可以使用RabbitMQ的方

式进行接口的限流,异步解耦通信,实现抢购时:“高并发限流”,“流量削峰”,但是在真实的场景中,远不

止这么简单,特别是在:“库存超卖”的问题上,是非常负载和难啃的。

抢购活动开始,前端大量的流量涌入,后端首先会:“查看商品当前的库存”,如果库存充足,则代表用户可以

抢购该商品,同时:“商品库存减去1”,并最终同步和更新到商品库存表中。理论上业务是这样进行中,但是现

实真的是这样吗?==假设商品库存是1,有A,B,C三个线程,在高并发情况下,A,B,C三个线程同时查询到:“查

看商品当前的库存” 三个线程获取的库存都是1,都认为库存是充足的,==最终三个线程执行了库存减去1的操

作,最终数据变为负数,即出现库存超卖的现象。

由此可见,“查库存”和“判断库存是否充足”和“库存减去1”是一个综合操作,是整个:”商城系统中高并发

抢购“的核心业务逻辑,也是多个并发线程访问的:”共享资源“,故而为了控制线程的并发问题,避免最终出

现库存超卖现象,我们需要在操作之前加入:”分布式锁“,确保高并发的情况下,同一时刻只能有一个线程获

取分布式锁,并执行核心的业务逻辑。

5. 小结

“高并发下并发访问共享资源”,在实际生产环境中是很常见的,此种业务场景在某种程度上虽然可以给企业带

来收益,但是同时也给应用系统带来了诸多问题,“数据不一致 和脏数据”便是其中典型的一种。

在传统的单体应用中遇到此种情况是用:“锁的机制”来解决,主要利用的是JDK自身提供的关键字

Synchronized和Lock来解决,然而,在服务集群,系统分布式部署的环境下,传统单体应用的“锁”机制却显得

有点力不从心,因此分布式锁出现了。

分布式锁主要用于在分布式系统架构下控制并发线程访问共享资源的方式,

九、基于数据库的方式实现分布式锁 - 乐观锁

1. 乐观锁简介

乐观锁是一种很 “佛系”的实现方式,总是认为不会产生并发的问题,故而每次从数据库获取数据时总认为不会

有其他线程对该数据进行修改,因此不会上锁,但是在更新时会判断其他线程在只之前有没有对该数据进行修

改,通常是采用版本号:“version”机制。

这种机制的执行流程如下:

假设有三个线程:A,B,C

  • 当前线程去取该数据记录时,会顺带把版本version的值取出来。最后在更新该数据记录时,将该version的取

值作为更新的条件。

public void getMemony(String userid,String money){
 // 查询用户信息
 UserAccount userAccount = UserAccountMapper.getId(userid);// B版本 0   A版本 0  C 版本 0
 // 用户开始体现
 UserAccountMapper.updateMinusMoney(userAccount.getTotalPrice -money);
    
    //update UserAccount set  version = userAccount.getVerion+1 where version = userAccount.getVerion
 // 用户体现记录表
 UserRecordMapper.saveRecord(userId,money);//1 80

}
  • 当更新成功后,同时将version的值+1,
  • 从而其他同时获取该数据记录的线程在更新时由于version的值以及不是当初获取的那个值,故而将更新失

败。

  • 从而避免了并发多线程访问共享数据时出现的数据不一致现象,

2. 乐观锁实战 - 余额体现

在很多的网站比如:微信,支付宝,网易支付等都有余额体现,比如:网易支付。

当用户在在用户账户余额的情况下,点击“体现申请”,即可申请的余额体现到指定的账户中,如下图:

当前用户在前端多次点击:“体现”按钮,将很有可能出现典型的并发现象,同一时刻产生多个体现余额的并发

请求,当这些请求到达后端接口是,正常情况下,接口会查询账户的余额,最终账户的余额的值为:“账户剩下

的金额” 减去 “申请体现的金额”。理想情况是这样,也没有问题,然后显示在高并发的情况下,当用户明明知

道自己的账户余额不够提取时,缺恶意的疯狂的点击:“体现”按钮,导致同一时刻产生了大量并发线程,由于

后端接口在处理每一个请求时,需要先取出当前账户的剩余余额,再去判断是否能够被提取,如果足够被提

取,则在当前账余额的基础上减去体现金额。最终将剩余的金额更新到:”账户余额“ 字段,这种逻辑处理很容

易产生并发安全问题,会出现”数据不一致“的现象,最终出现是账户月字段的值变成:负数。

2.1. 新建一个项目springboot

依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zheng.travel.lock</groupId>
    <artifactId>xq_pugs_middle_lock</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>xq_pugs_middle_lock</name>
    <description>xq_pugs_middle_lock</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!--guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.0.1-jre</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>

        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.8.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.10.0</version>
        </dependency>
        <!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--zookeeper-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.3</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-log4j12</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.3.0</version>
        </dependency>
        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.8</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.7</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
配置文件

application.properties配置

#指定应用访问的上下文以及端口
server.context-path=/middle
server.port=8887


#数据库访问配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xq_pug_travels?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=mkxiaoer


# mybatis-plus配置
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:/mapper/*.xml

注解
package com.zheng.travel.lock;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "com.zheng.travel.lock.mapper")
public class XqPugsMiddleLockApplication {

    public static void main(String[] args) {
        SpringApplication.run(XqPugsMiddleLockApplication.class, args);
    }

}

2.2. 新建一个用户余额表和用户提现记录表

user_account 用户余额表

CREATE TABLE `user_account` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) NOT NULL COMMENT '用户账户id',
  `amount` decimal(10,4) NOT NULL COMMENT '账户余额',
  `version` int(11) DEFAULT '1' COMMENT '版本号字段',
  `is_active` tinyint(11) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='用户账户表'

user_account_record 用户提现记录表

CREATE TABLE `user_account_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `account_id` int(11) NOT NULL COMMENT '账户表主键id',
  `money` decimal(10,4) DEFAULT NULL COMMENT '提现成功时记录的金额',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=423 DEFAULT CHARSET=utf8 COMMENT='用户每次提现时的金额记录表'

2.3. 创建对应文件

entity
package com.zheng.travel.lock.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;

import java.math.BigDecimal;

/**
 * 用户账户实体
 */
@Data
@ToString
@TableName("user_account")
public class UserAccount {
    @TableId(type = IdType.AUTO)
    private Integer id; //主键Id
    private Integer userId;//用户账户id
    private BigDecimal amount;//账户余额
    private Integer version; //版本号
    private Byte isActive; //是否有效账户
}
package com.zheng.travel.lock.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;

import java.math.BigDecimal;
import java.util.Date;

/**
 * 用户每次提现时金额记录实体
 */
@Data
@ToString
@TableName("user_account_record")
public class UserAccountRecord {
    @TableId(type = IdType.AUTO)
    private Integer id; //主键id
    private Integer accountId; //账户记录主键id
    private BigDecimal money; //提现金额
    private Date createTime; //提现成功时间
}
mapper
package com.zheng.travel.lock.mapper;

import com.zheng.travel.lock.model.UserAccount;
import org.apache.ibatis.annotations.Param;

public interface UserAccountMapper {
    //根据主键id查询
    UserAccount selectByPrimaryKey(Integer id);

    //根据用户账户Id查询
    UserAccount selectByUserId(@Param("userId") Integer userId);

    //更新账户金额
    int updateAmount(@Param("money") Double money, @Param("id") Integer id);

    //根据主键id跟version进行更新
    int updateByPKVersion(@Param("money") Double money, @Param("id") Integer id, @Param("version") Integer version);

    //根据用户id查询记录-for update方式
    UserAccount selectByUserIdLock(@Param("userId") Integer userId);

    //更新账户金额-悲观锁的方式
    int updateAmountLock(@Param("money") Double money, @Param("id") Integer id);
}
package com.zheng.travel.lock.mapper;


import com.zheng.travel.lock.model.UserAccountRecord;

public interface UserAccountRecordMapper {
    //插入记录
    int insert(UserAccountRecord record);
    //根据主键id查询
    UserAccountRecord selectByPrimaryKey(Integer id);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--xml版本与命名空间定义-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--定义所在的命名空间-->
<mapper namespace="com.zheng.travel.lock.mapper.UserAccountMapper" >
    <!--查询结果集映射-->
    <resultMap id="BaseResultMap" type="com.zheng.travel.lock.model.UserAccount" >
        <id column="id" property="id" jdbcType="INTEGER" />
        <result column="user_id" property="userId" jdbcType="INTEGER" />
        <result column="amount" property="amount" jdbcType="DECIMAL" />
        <result column="version" property="version" jdbcType="INTEGER" />
        <result column="is_active" property="isActive" jdbcType="TINYINT" />
    </resultMap>

    <!--查询的sql片段-->
    <sql id="Base_Column_List" >
        id, user_id, amount, version, is_active
    </sql>

    <!--根据主键id查询-->
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
        select
        <include refid="Base_Column_List" />
        from user_account
        where id = #{id,jdbcType=INTEGER}
    </select>

    <!--根据用户账户id查询记录-->
    <select id="selectByUserId" resultType="com.zheng.travel.lock.model.UserAccount">
        SELECT <include refid="Base_Column_List"/>
        FROM user_account
        WHERE is_active=1 AND user_id=#{userId}
    </select>

    <!--根据主键id更新账户余额-->
    <update id="updateAmount">
        UPDATE user_account SET amount = amount - #{money}
        WHERE is_active=1 AND id=#{id}
    </update>

    <!--根据主键id跟version更新记录-->
    <update id="updateByPKVersion">
        update user_account set amount = amount - #{money},version=version+1
        where id = #{id} and version=#{version} and amount >0 and (amount - #{money})>=0
    </update>

    <!--根据用户id查询-用于悲观锁-->
    <select id="selectByUserIdLock" resultType="com.zheng.travel.lock.model.UserAccount">
        SELECT <include refid="Base_Column_List"/>
        FROM user_account
        WHERE user_id=#{userId} FOR UPDATE
    </select>

    <!--根据主键id更新账户余额-悲观锁的方式-->
    <update id="updateAmountLock">
        UPDATE user_account SET amount = amount - #{money}
        WHERE is_active=1 AND id=#{id} and amount >0 and (amount - #{money})>=0
    </update>

</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!--xml版本与命名空间定义-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--定义所在的命名空间-->
<mapper namespace="com.zheng.travel.lock.mapper.UserAccountRecordMapper" >
    <!--查询结果集映射-->
    <resultMap id="BaseResultMap" type="com.zheng.travel.lock.model.UserAccountRecord" >
        <id column="id" property="id" jdbcType="INTEGER" />
        <result column="account_id" property="accountId" jdbcType="INTEGER" />
        <result column="money" property="money" jdbcType="DECIMAL" />
        <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
    </resultMap>

    <!--查询的sql片段-->
    <sql id="Base_Column_List" >
        id, account_id, money, create_time
    </sql>

    <!--根据主键id查询-->
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
        select
        <include refid="Base_Column_List" />
        from user_account_record
        where id = #{id,jdbcType=INTEGER}
    </select>

    <!--插入记录-->
    <insert id="insert" parameterType="com.zheng.travel.lock.model.UserAccountRecord" >
        insert into user_account_record (id, account_id, money, create_time)
        values (#{id,jdbcType=INTEGER}, #{accountId,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL},
                #{createTime,jdbcType=TIMESTAMP})
    </insert>

</mapper>
service
package com.zheng.travel.lock.service.db; /**
 * Created by Administrator on 2019/4/17.
 */

import com.zheng.travel.lock.dto.UserAccountDto;
import com.zheng.travel.lock.mapper.UserAccountMapper;
import com.zheng.travel.lock.mapper.UserAccountRecordMapper;
import com.zheng.travel.lock.model.UserAccount;
import com.zheng.travel.lock.model.UserAccountRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Date;

/**
 * 基于数据库级别的乐观、悲观锁服务
 * @Author:debug (SteadyJack)
 * @Date: 2019/4/17 20:36
 **/
@Service
public class DataBaseLockService {
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(DataBaseLockService.class);
    //定义“用户账户余额实体”Mapper操作接口
    @Autowired
    private UserAccountMapper userAccountMapper;
    //定义“用户成功申请提现时金额记录”Mapper操作接口
    @Autowired
    private UserAccountRecordMapper userAccountRecordMapper;

    /**
     * 用户账户提取金额处理
     * @param dto
     * @throws Exception
     */
    public void takeMoney(UserAccountDto dto) throws Exception{
        //查询用户账户实体记录
        UserAccount userAccount=userAccountMapper.selectByUserId(dto.getUserId());
        //判断实体记录是否存在 以及 账户余额是否足够被提现
        if (userAccount!=null && userAccount.getAmount().doubleValue()-dto.getAmount()>0){
            //如果足够被提现,则更新现有的账户余额
            userAccountMapper.updateAmount(dto.getAmount(),userAccount.getId());
            //同时记录提现成功时的记录
            UserAccountRecord record=new UserAccountRecord();
            //设置提现成功时的时间
            record.setCreateTime(new Date());
            //设置账户记录主键id
            record.setAccountId(userAccount.getId());
            //设置成功申请提现时的金额
            record.setMoney(BigDecimal.valueOf(dto.getAmount()));
            //插入申请提现金额历史记录
            userAccountRecordMapper.insert(record);
            //打印日志
            log.info("当前待提现的金额为:{} 用户账户余额为:{}",dto.getAmount(),userAccount.getAmount());
        }else {
            throw new Exception("账户不存在或账户余额不足!");
        }
    }


    /**
     * 乐观锁处理方式
     * @param dto
     * @throws Exception
     */
    public void takeMoneyWithLock(UserAccountDto dto) throws Exception{
        //查询用户账户实体记录
        UserAccount userAccount=userAccountMapper.selectByUserId(dto.getUserId());
        //判断实体记录是否存在 以及 账户余额是否足够被提现
        if (userAccount!=null && userAccount.getAmount().doubleValue()-dto.getAmount()>0){
            //如果足够被提现,则更新现有的账户余额 - 采用version版本号机制
            int res=userAccountMapper.updateByPKVersion(dto.getAmount(),userAccount.getId(),userAccount.getVersion());
            //只有当更新成功时(此时res=1,即数据库执行更细语句之后数据库受影响的记录行数)
            if (res>0){
                //同时记录提现成功时的记录
                UserAccountRecord record=new UserAccountRecord();
                //设置提现成功时的时间
                record.setCreateTime(new Date());
                //设置账户记录主键id
                record.setAccountId(userAccount.getId());
                //设置成功申请提现时的金额
                record.setMoney(BigDecimal.valueOf(dto.getAmount()));
                //插入申请提现金额历史记录
                userAccountRecordMapper.insert(record);
                //打印日志
                log.info("当前待提现的金额为:{} 用户账户余额为:{}",dto.getAmount(),userAccount.getAmount());
            }
        }else {
            throw new Exception("账户不存在或账户余额不足!");
        }
    }




    /**
     * 悲观锁处理方式
     * @param dto
     * @throws Exception
     */
    public void takeMoneyWithLockNegative(UserAccountDto dto) throws Exception{
        //查询用户账户实体记录 - for update的方式
        UserAccount userAccount=userAccountMapper.selectByUserIdLock(dto.getUserId());
        //判断实体记录是否存在 以及 账户余额是否足够被提现
        if (userAccount!=null && userAccount.getAmount().doubleValue()-dto.getAmount()>0){
            //如果足够被提现,则更新现有的账户余额 - 采用version版本号机制
            int res=userAccountMapper.updateAmountLock(dto.getAmount(),userAccount.getId());
            //只有当更新成功时(此时res=1,即数据库执行更细语句之后数据库受影响的记录行数)
            if (res>0){
                //同时记录提现成功时的记录
                UserAccountRecord record=new UserAccountRecord();
                //设置提现成功时的时间
                record.setCreateTime(new Date());
                //设置账户记录主键id
                record.setAccountId(userAccount.getId());
                //设置成功申请提现时的金额
                record.setMoney(BigDecimal.valueOf(dto.getAmount()));
                //插入申请提现金额历史记录
                userAccountRecordMapper.insert(record);
                //打印日志
                log.info("悲观锁处理方式-当前待提现的金额为:{} 用户账户余额为:{}",dto.getAmount(),userAccount.getAmount());
            }
        }else {
            throw new Exception("悲观锁处理方式-账户不存在或账户余额不足!");
        }
    }
}
controller
package com.zheng.travel.lock.controller.db; /**
 * Created by Administrator on 2019/4/17.
 */

import com.zheng.travel.lock.common.R;
import com.zheng.travel.lock.common.StatusCode;
import com.zheng.travel.lock.dto.UserAccountDto;
import com.zheng.travel.lock.service.db.DataBaseLockService;
import io.swagger.annotations.Api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 基于数据库的乐观悲观锁
 **/
@RestController
@Api(tags="数据库的乐观锁和悲观锁")
public class DataBaseLockController {
    //定义日志
    private static final Logger log = LoggerFactory.getLogger(DataBaseLockController.class);
    //定义请求前缀
    private static final String prefix = "db";
    //定义核心逻辑处理服务类
    @Autowired
    private DataBaseLockService dataBaseLockService;

    /**
     * 用户账户余额提现申请
     *
     * @param dto
     * @return
     */
    @RequestMapping(value = prefix + "/money/take", method = RequestMethod.GET)
    public R takeMoney(UserAccountDto dto) {
        if (dto.getAmount() == null || dto.getUserId() == null) {
            return new R(StatusCode.InvalidParams);
        }
        R response = new R(StatusCode.Success);
        try {
            //不加锁的情况
            dataBaseLockService.takeMoney(dto);
            //加乐观锁的情况
            //dataBaseLockService.takeMoneyWithLock(dto);
            //加悲观锁的情况
            //dataBaseLockService.takeMoneyWithLockNegative(dto);
        } catch (Exception e) {
            response = new R(StatusCode.Fail.getCode(), e.getMessage());
        }
        return response;
    }
}
jmeter压力测试看结果

启动

汉化

皮肤外观

新建线程组

添加并发请求的接口

压力测试以后可以得出结论属于重复体现

十、基于数据库的方式实现分布式锁 - 悲观锁

1. 悲观锁简介

悲观锁:是一种“消极,悲观”的处理方式,它总是假设事情的发生是在:“最坏的情况”,即每次并发线程在

获取数据的时候会认为其他线程会对数据进行修改,故而每次获取数据时都会上锁,而其他线程的防卫数据的时

候就会发生阻塞的想象。最终只有当前线程是否该共享资源的锁,其他线程才能获取锁,并对共享资源进行操

作。

2. 具体实现

在传统关系型数据库中就用到了很多类似悲观锁的机制,如:行锁,表锁和共享锁和排它锁等,都是在进行操作

之前先上锁,Java中的Synchorinzed和Lock也都参考了数据库的悲观锁来设计的。对于数据库级别的悲观锁而

言,目前Oracle和MYSQL数据库都是采用如下SQL的方式进行实现。

select 字段列表 from 数据表 for update

假设这个时候高并发产生了多个线程比如A,B,C时,3个线程同时前往数据库查询共享的数据记录,由于数据库引

擎的作用,同一时刻将只会有一个线程如A线程获取该数据库记录的锁(在MYSQL中属于“行”级别的锁),其

他的两个线程B和C将处在一直等待的状态,直到A线程对该数据库记录操作完毕,并提交事务之后才会释放锁。

之后B和C的其中一个线程才能成功获取锁,并执行相应的操作,数据库级别的悲观锁实现流程如下:

从上图可以看出来,当请求量很大的时候,由于产生了的每个线程都在查询数据库的时候都需要上锁,而且同一

时刻也只能有一个线程上锁成功,只有当该线程对该共享资源操作完毕并释放该锁之后,其他正在等待的线程才

能获取到锁。

采用这种方式将会造成大量的先发生堵塞现象,在某种成都上会给数据库服务器造成一定的压力,从这个角度上

思考,基于数据库的悲观锁适合于并发量不大的情况,特别是在:“读”请求数据量不大的情况。

3. 实战开发

4. 小结

  • 乐观锁,(版本号的相互排斥)主要采用的是版本号Version的机制实现,故而在高并发产生多线程时,同一时刻只有一个线程能获取到”锁“并成功操作共享资源,而其他的线程将获取失败,而且是永久性地失败,从这个角度来看,这种方式虽然可以控制并发线程共享资源访问,==但是却牺牲了系统的吞吐性能。另外,乐观锁,主要是通过version字段对共享数据进行跟踪和控制,其最终一个实现步骤带上version进行匹配,同时执行version+1的更新操作,故而在并发多线程需要频繁”写“数据库时,是会严重影响数据库的性能的,从这个角度上看,乐观锁比较适合 ”写少读多“的业务场景。
  • 悲观锁,由于是建立在数据库底层搜索引擎的基础上,并使用 select * from where id =1 table for update 的查询语句对共享资源加锁,故而在高并发多线程请求,特别是 读 的请求时,将对数据的性能带来严重的影响,因为在同一时刻产生的多线程中将只有一个线程获取到锁,而其他的线程将处于堵塞状态,直到该线程释放了锁,使用悲观锁要注意的是,如果使用不恰当很可能产生死锁的现象,即两个或者多个线程同时处于等待获取对方的资源的锁的状态,故而”悲观锁“更适用于 读少写多的业务场景。

5. 上锁目的

让多线程变成 一种有序执行的方式。性能减低,保护数据的共享资源数据的一致性问题。

十一、基于Zookeeper的方式实现分布式锁 - 商品抢购

案例场景:

  • 商品表
  • 商品库存表
  • 抢购商品下单

1. 简介

Zookeeper是一款开源的分布式服务协调中间件,是由雅虎团队研发而来,其设计的初衷是开发一个通用的,无

单点问题的分布式协调框架,采用统一的协调管理方式更好地管理各个子系统,从而让开发者将更多的经理集中

在业务逻辑处理上。最终整个分布式系统看上去就想是一个大型的动物园,而这个中间件正好用来协调分布式环

境中的各个子系统,zookeeper因此而得名

2. Zookeeper可以用来干嘛?

官网:Apache ZooKeeper

  • 统一配置管理:将每个子系统都需要配置的文件统一放到zookeeper中的znode节点中。
  • 统一命名服务:通过给存放在znode上的资源进行统一命名,各个子系统便可以通过名字获取到节点上响应的资源。
  • 分布式锁:通过创建于该共享资源相关的”顺序临时节点“与动态watcher监听机制,而从监控多线程对共享资源的并发访问。(排队取号的机制,如果不明白可以百度找一些文章,其实更加建议结合redis去理解会更好)
    • 为什么一定是:顺序临时节点,因为可以保证每个线程执行的都是唯一的。
    • watcher机制机制:把最小的序号删除以后,一定要把List第一个元素给删除,让其他的线程继续获取锁。
  • 集群状态:通过动态地感知节点的增加,删除,从而保证集群下的相关节点数据主,副本数据的一致。

3. Zookeeper分布式锁流程

zookeeper实现分布式锁主要是通过创建与共享资源相关的:“顺序临时节点” 并采用其提供的Watcher监听机

制,控制多线程对共享资源的并发访问,整体如下:

4. SpringBoot整合Zookeeper

下载地址:Index of /dist/zookeeper

4.1. 依赖

<!--zookeeper-->
<dependency>
    <groupId>org.apache.zookeeper</groupId>

    <artifactId>zookeeper</artifactId>

    <version>3.6.3</version>

    <exclusions>
        <exclusion>
            <artifactId>slf4j-log4j12</artifactId>

            <groupId>org.slf4j</groupId>

        </exclusion>

    </exclusions>

</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
    <groupId>org.apache.curator</groupId>

    <artifactId>curator-framework</artifactId>

    <version>4.3.0</version>

</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>

    <artifactId>curator-recipes</artifactId>

    <version>4.3.0</version>

</dependency>

4.2. 配置

#zookeeper配置
zk.host=127.0.0.1:2181
zk.namespace=travel_middle_lock

4.3. 配置初始化

package com.zheng.travel.lock.config; /**
 * Created by Administrator on 2019/3/13.
 */

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 通用化配置
 **/
@Configuration
public class CuratorFrameworkConfiguration {

    //读取环境变量的实例
    //@Autowired
    //private Environment env;
    @Value("${zk.host}")
    private String host;
    @Value("${zk.namespace}")
    private String namespace;


    //自定义注入Bean-ZooKeeper高度封装过的客户端Curator实例
    @Bean
    public CuratorFramework curatorFramework() {
        //创建CuratorFramework实例
        //(1)创建的方式是采用工厂模式进行创建;
        //(2)指定了客户端连接到ZooKeeper服务端的策略:这里是采用重试的机制(5次,每次间隔1s)
//        CuratorFramework curatorFramework= CuratorFrameworkFactory.builder()
//                .connectString(env.getProperty("zk.host")).namespace(env.getProperty("zk.namespace"))
//                .retryPolicy(new RetryNTimes(5,1000)).build();
        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(host).namespace(namespace)
                .retryPolicy(new RetryNTimes(5, 1000)).build();
        curatorFramework.start();
        //返回CuratorFramework实例
        return curatorFramework;
    }

}

4.4. 用户注册实现分布式锁

package com.debug.middleware.server.service.lock;/**
 * Created by Administrator on 2019/4/20.
 */

import com.debug.middleware.model.entity.UserReg;
import com.debug.middleware.model.mapper.UserRegMapper;
import com.debug.middleware.server.controller.lock.dto.UserRegDto;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 处理用户注册信息提交服务Service
 **/
@Service
public class UserRegService {
    //定义日志实例
    private static final Logger log= LoggerFactory.getLogger(UserRegService.class);
    //定义用户注册Mapper操作接口实例
    @Autowired
    private UserRegMapper userRegMapper;


    //定义ZooKeeper客户端CuratorFramework实例
    @Autowired
    private CuratorFramework client;
    //ZooKeeper分布式锁的实现原理是由ZNode节点的创建与删除跟监听机制构成的
    //而ZNoe节点将对应一个具体的路径-跟Unix文件夹路径类似-需要以 / 开头
    private static final String pathPrefix="/middleware/zkLock/";

    /**
     * 处理用户提交注册的请求-加ZooKeeper分布式锁
     * @param dto
     * @throws Exception
     */
    public void userRegWithZKLock(UserRegDto dto) throws Exception{
        //创建ZooKeeper互斥锁组件实例,需要将监控用的客户端实例、精心构造的共享资源 作为构造参数
        InterProcessMutex mutex=new InterProcessMutex(client,pathPrefix+dto.getUserName()+"-lock");
        try {
            //采用互斥锁组件尝试获取分布式锁-其中尝试的最大时间在这里设置为10s
            //当然,具体的情况需要根据实际的业务而定
            if (mutex.acquire(10L, TimeUnit.SECONDS)){
                //TODO:真正的核心处理逻辑

                //根据用户名查询用户实体信息
                UserReg reg=userRegMapper.selectByUserName(dto.getUserName());
                //如果当前用户名还未被注册,则将当前用户信息注册入数据库中
                if (reg==null){
                    log.info("---加了ZooKeeper分布式锁---,当前用户名为:{} ",dto.getUserName());
                    //创建用户注册实体信息
                    UserReg entity=new UserReg();
                    //将提交的用户注册请求实体信息中对应的字段取值
                    //复制到新创建的用户注册实体的相应字段中
                    BeanUtils.copyProperties(dto,entity);
                    //设置注册时间
                    entity.setCreateTime(new Date());
                    //插入用户注册信息
                    userRegMapper.insertSelective(entity);

                }else {
                    //如果用户名已被注册,则抛出异常
                    throw new Exception("用户信息已经存在!");
                }
            }else{
                throw new RuntimeException("获取ZooKeeper分布式锁失败!");
            }
        }catch (Exception e){
            throw e;
        }finally {
            //TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
            mutex.release();
        }
    }

}

5. 实现商品抢购扣减库存

-- ----------------------------
-- Table structure for book_rob
-- ----------------------------
DROP TABLE IF EXISTS `book_rob`;
CREATE TABLE `book_rob` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `book_no` varchar(255) NOT NULL COMMENT '商品编号',
  `rob_time` datetime DEFAULT NULL COMMENT '抢购时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=153 DEFAULT CHARSET=utf8 COMMENT='商品抢购记录';

-- ----------------------------
-- Records of book_rob
-- ----------------------------
INSERT INTO `book_rob` VALUES ('2', '10010', 'BS20190421001', '2019-04-22 22:49:05');
INSERT INTO `book_rob` VALUES ('147', '10040', 'BS20190421001', '2019-04-22 23:28:05');
INSERT INTO `book_rob` VALUES ('148', '10042', 'BS20190421001', '2019-04-22 23:28:05');
INSERT INTO `book_rob` VALUES ('149', '10041', 'BS20190421001', '2019-04-22 23:28:05');
INSERT INTO `book_rob` VALUES ('150', '10045', 'BS20190421001', '2019-04-22 23:28:05');
INSERT INTO `book_rob` VALUES ('151', '10043', 'BS20190421001', '2019-04-22 23:28:05');
INSERT INTO `book_rob` VALUES ('152', '10044', 'BS20190421001', '2019-04-22 23:28:05');



-- ----------------------------
-- Table structure for book_stock
-- ----------------------------
DROP TABLE IF EXISTS `book_stock`;
CREATE TABLE `book_stock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `book_no` varchar(255) NOT NULL COMMENT '商品编号',
  `stock` int(255) NOT NULL COMMENT '库存',
  `is_active` tinyint(255) DEFAULT '1' COMMENT '是否上架(1=是;0=否)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='商品库存表';

-- ----------------------------
-- Records of book_stock
-- ----------------------------
INSERT INTO `book_stock` VALUES ('1', 'BS20190421001', '4', '1');

bean

package com.debug.middleware.model.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Date;

//商品抢购记录实体
@Data
@ToString
public class BookRob {
    private Integer id;    //主键id
    private Integer userId;//用户id
    private String bookNo; //商品编号
    private Date robTime;  //抢购时间
}

package com.debug.middleware.model.entity;

import lombok.Data;
import lombok.ToString;

//商品库存实体
@Data
@ToString
public class BookStock {
    private Integer id;   //主键Id
    private String bookNo;//商品编号
    private Integer stock;//存库
    private Byte isActive;//是否上架
}

mapper

package com.debug.middleware.model.mapper;

import com.debug.middleware.model.entity.BookRob;
import org.apache.ibatis.annotations.Param;
//商品抢购成功的记录实体Mapper操作接口
public interface BookRobMapper {
    //插入抢购成功的记录信息
    int insertSelective(BookRob record);
    //统计每个用户每本书的抢购数量
    //用于判断用户是否抢购过该商品
    int countByBookNoUserId(@Param("userId") Integer userId,@Param("bookNo") String bookNo);
}
package com.debug.middleware.model.mapper;

import com.debug.middleware.model.entity.BookStock;
import org.apache.ibatis.annotations.Param;
//商品库存实体操作接口Mapper
public interface BookStockMapper {
    //根据商品编号查询
    BookStock selectByBookNo(@Param("bookNo") String bookNo);
    //更新商品库存-不加锁
    int updateStock(@Param("bookNo") String bookNo);
    //更新商品库存-加锁
    int updateStockWithLock(@Param("bookNo") String bookNo);
}

controller

package com.debug.middleware.server.controller.lock;/**
 * Created by Administrator on 2019/4/17.
 */

import com.debug.middleware.api.enums.StatusCode;
import com.debug.middleware.api.response.BaseResponse;
import com.debug.middleware.server.controller.lock.dto.BookRobDto;
import com.debug.middleware.server.service.lock.BookRobService;
import org.assertj.core.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 商品抢购Controller
 * @Author:debug (SteadyJack)
 * @Date: 2019/4/21 23:31
 **/
@RestController
public class BookRobController {
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(BookRobController.class);
    //定义请求前缀
    private static final String prefix="book/rob";
    //定义核心逻辑处理服务类
    @Autowired
    private BookRobService bookRobService;

    /**
     * 用户抢购商品请求
     * @param dto
     * @return
     */
    @RequestMapping(value = prefix+"/request",method = RequestMethod.GET)
    public BaseResponse takeMoney(BookRobDto dto){
        if (Strings.isNullOrEmpty(dto.getBookNo()) || dto.getUserId()==null || dto.getUserId()<=0){
            return new BaseResponse(StatusCode.InvalidParams);
        }
        BaseResponse response=new BaseResponse(StatusCode.Success);
        try {
            //不加锁的情况
            //bookRobService.robWithNoLock(dto);

            //加ZooKeeper分布式锁的情况
            //bookRobService.robWithZKLock(dto);

            //加Redisson分布式锁的情况
            bookRobService.robWithRedisson(dto);
        }catch (Exception e){
            response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
        }
        return response;
    }
}

service

package com.debug.middleware.server.service.lock;/**
 * Created by Administrator on 2019/4/17.
 */

import com.debug.middleware.model.entity.*;
import com.debug.middleware.model.mapper.*;
import com.debug.middleware.server.controller.lock.dto.BookRobDto;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 商品抢购服务
 * @Author:debug (SteadyJack)
 * @Date: 2019/4/21 23:36
 **/
@Service
public class BookRobService {
    //定义日志实例
    private static final Logger log= LoggerFactory.getLogger(BookRobService.class);
    //定义商品库存实体操作接口Mapper实例
    @Autowired
    private BookStockMapper bookStockMapper;
    //定义商品抢购实体操作接口Mapper实例
    @Autowired
    private BookRobMapper bookRobMapper;

    /**
     * 处理商品抢购逻辑-不加分布式锁
     * @param dto
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public void robWithNoLock(BookRobDto dto) throws Exception{
        //根据商品编号查询记录
        BookStock stock=bookStockMapper.selectByBookNo(dto.getBookNo());
        //统计每个用户每本书的抢购数量
        int total=bookRobMapper.countByBookNoUserId(dto.getUserId(),dto.getBookNo());

        //商品记录存在、库存充足,而且用户还没抢购过本书,则代表当前用户可以抢购
        if (stock!=null && stock.getStock()>0 && total<=0){
            log.info("---处理商品抢购逻辑-不加分布式锁---,当前信息:{} ",dto);

            //当前用户抢购到商品,库存减一
            int res=bookStockMapper.updateStock(dto.getBookNo());
            //更新库存成功后,需要添加抢购记录
            if (res>0){
                //创建商品抢购记录实体信息
                BookRob entity=new BookRob();
                //将提交的用户抢购请求实体信息中对应的字段取值
                //复制到新创建的商品抢购记录实体的相应字段中
                BeanUtils.copyProperties(dto,entity);
                //设置抢购时间
                entity.setRobTime(new Date());
                //插入用户注册信息
                bookRobMapper.insertSelective(entity);
            }
        }else {
            //如果不满足上述的任意一个if条件,则抛出异常
            throw new Exception("该商品库存不足!");
        }
    }





    //定义ZooKeeper客户端CuratorFramework实例
    @Autowired
    private CuratorFramework client;
    //ZooKeeper分布式锁的实现原理是由ZNode节点的创建与删除跟监听机制构成的
    //而ZNoe节点将对应一个具体的路径-跟Unix文件夹路径类似-需要以 / 开头
    private static final String pathPrefix="/middleware/zkLock/";

    /**
     * 处理商品抢购逻辑-加ZooKeeper分布式锁
     * @param dto
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public void robWithZKLock(BookRobDto dto) throws Exception{
        //创建ZooKeeper互斥锁组件实例,需要将CuratorFramework实例、精心构造的共享资源 作为构造参数
        InterProcessMutex mutex=new InterProcessMutex(client,pathPrefix+dto.getBookNo()+dto.getUserId()+"-lock");
        try {
            //采用互斥锁组件尝试获取分布式锁-其中尝试的最大时间在这里设置为15s
            //当然,具体的情况需要根据实际的业务而定
            if (mutex.acquire(15L, TimeUnit.SECONDS)){
                //TODO:真正的核心处理逻辑

                //根据商品编号查询记录
                BookStock stock=bookStockMapper.selectByBookNo(dto.getBookNo());
                //统计每个用户每本书的抢购数量
                int total=bookRobMapper.countByBookNoUserId(dto.getUserId(),dto.getBookNo());

                //商品记录存在、库存充足,而且用户还没抢购过本书,则代表当前用户可以抢购
                if (stock!=null && stock.getStock()>0 && total<=0){
                    log.info("---处理商品抢购逻辑-加ZooKeeper分布式锁---,当前信息:{} ",dto);

                    //当前用户抢购到商品,库存减一
                    int res=bookStockMapper.updateStock(dto.getBookNo());
                    //更新库存成功后,需要添加抢购记录
                    if (res>0){
                        //创建商品抢购记录实体信息
                        BookRob entity=new BookRob();
                        //将提交的用户抢购请求实体信息中对应的字段取值
                        //复制到新创建的商品抢购记录实体的相应字段中
                        entity.setUserId(dto.getUserId());
                        entity.setBookNo(dto.getBookNo());
                        //设置抢购时间
                        entity.setRobTime(new Date());
                        //插入用户注册信息
                        bookRobMapper.insertSelective(entity);
                    }
                }else {
                    //如果不满足上述的任意一个if条件,则抛出异常
                    throw new Exception("该商品库存不足!");
                }

            }else{
                throw new RuntimeException("获取ZooKeeper分布式锁失败!");
            }
        }catch (Exception e){
            throw e;
        }finally {
            //TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
            mutex.release();
        }
    }

    //定义Redisson的客户端操作实例
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 处理商品抢购逻辑-加Redisson分布式锁
     * @param dto
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public void robWithRedisson(BookRobDto dto) throws Exception{
        final String lockName="redissonTryLock-"+dto.getBookNo()+"-"+dto.getUserId();
        RLock lock=redissonClient.getLock(lockName);
        try {
            Boolean result=lock.tryLock(100,10,TimeUnit.SECONDS);
            if (result){
                //TODO:真正的核心处理逻辑

                //根据商品编号查询记录
                BookStock stock=bookStockMapper.selectByBookNo(dto.getBookNo());
                //统计每个用户每本书的抢购数量
                int total=bookRobMapper.countByBookNoUserId(dto.getUserId(),dto.getBookNo());

                //商品记录存在、库存充足,而且用户还没抢购过本书,则代表当前用户可以抢购
                if (stock!=null && stock.getStock()>0 && total<=0){
                    //当前用户抢购到商品,库存减一
                    int res=bookStockMapper.updateStockWithLock(dto.getBookNo());
                    //如果允许商品超卖-达成饥饿营销的目的,则可以调用下面的方法
                    //int res=bookStockMapper.updateStock(dto.getBookNo());

                    //更新库存成功后,需要添加抢购记录
                    if (res>0){
                        //创建商品抢购记录实体信息
                        BookRob entity=new BookRob();
                        //将提交的用户抢购请求实体信息中对应的字段取值
                        //复制到新创建的商品抢购记录实体的相应字段中
                        entity.setBookNo(dto.getBookNo());
                        entity.setUserId(dto.getUserId());
                        //设置抢购时间
                        entity.setRobTime(new Date());
                        //插入用户注册信息
                        bookRobMapper.insertSelective(entity);

                        log.info("---处理商品抢购逻辑-加Redisson分布式锁---,当前线程成功抢到商品:{} ",dto);
                    }
                }else {
                    //如果不满足上述的任意一个if条件,则抛出异常
                    throw new Exception("该商品库存不足!");
                }
            }else{
                throw new Exception("----获取Redisson分布式锁失败!----");
            }
        }catch (Exception e){
            throw e;
        }finally {
            //TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
            if (lock!=null){
                lock.unlock();

                //在某些严格的业务场景下,也可以调用强制释放分布式锁的方法
                //lock.forceUnlock();
            }
        }
    }
}

十二、基于Zookeeper 完成 实现商品抢购扣减库存

1. 案例场景

2. 流程架构

3. 表设计

商品表 —db

CREATE TABLE `kss_product` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主健',
  `product_title` varchar(20) DEFAULT NULL COMMENT '商品标题',
  `product_price` varchar(20) DEFAULT NULL COMMENT '商品价格',
  `product_no` varchar(20) DEFAULT NULL COMMENT '商品编号',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

商品表用户记录表:

CREATE TABLE `kss_product_records` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主健',
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `product_no` varchar(20) DEFAULT NULL COMMENT '商品编号',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

商品库存表 –db / redis – 数据的一致性的问题

CREATE TABLE `kss_product_stock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主健',
  `product_no` varchar(20) DEFAULT NULL COMMENT '商品编号',
  `stock` int(11) DEFAULT NULL COMMENT '商品库存',
  `is_active` int(1) DEFAULT NULL COMMENT '是否上架(1=是;0=否)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

4. 具体实现

依赖

 <!--zookeeper-->
 <dependency>
     <groupId>org.apache.zookeeper</groupId>

     <artifactId>zookeeper</artifactId>

     <version>3.6.3</version>

     <exclusions>
         <exclusion>
             <artifactId>slf4j-log4j12</artifactId>

             <groupId>org.slf4j</groupId>

         </exclusion>

     </exclusions>

 </dependency>

 <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
 <dependency>
     <groupId>org.apache.curator</groupId>

     <artifactId>curator-framework</artifactId>

     <version>4.3.0</version>

 </dependency>

 <dependency>
     <groupId>org.apache.curator</groupId>

     <artifactId>curator-recipes</artifactId>

     <version>4.3.0</version>

 </dependency>

配置

 #zookeeper配置
 zk.host=127.0.0.1:2181
 zk.namespace=pug_middle_lock

配置初始化

 package com.zheng.travel.lock.config; /**
  * Created by Administrator on 2019/3/13.
  */
 
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.CuratorFrameworkFactory;
 import org.apache.curator.retry.RetryNTimes;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.Environment;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 /**
  * 通用化配置
  *
  * @Date: 2019/3/13 8:38
  * @Link:微信-debug0868 QQ-1948831260
  **/
 @Configuration
 public class CuratorFrameworkConfiguration {
 
     //读取环境变量的实例
     //@Autowired
     //private Environment env;
     @Value("${zk.host}")
     private String host;
     @Value("${zk.namespace}")
     private String namespace;
 
 
     //自定义注入Bean-ZooKeeper高度封装过的客户端Curator实例
     @Bean
     public CuratorFramework curatorFramework() {
         //创建CuratorFramework实例
         //(1)创建的方式是采用工厂模式进行创建;
         //(2)指定了客户端连接到ZooKeeper服务端的策略:这里是采用重试的机制(5次,每次间隔1s)
 //        CuratorFramework curatorFramework= CuratorFrameworkFactory.builder()
 //                .connectString(env.getProperty("zk.host")).namespace(env.getProperty("zk.namespace"))
 //                .retryPolicy(new RetryNTimes(5,1000)).build();
         CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                 .connectString(host).namespace(namespace)
                 .retryPolicy(new RetryNTimes(5, 1000)).build();
         curatorFramework.start();
         //返回CuratorFramework实例
         return curatorFramework;
     }
 
 }

bean

package com.debug.middleware.model.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Date;

//商品抢购记录实体
@Data
@ToString
public class ProductRecords {
    private Integer id;    //主键id
    private Integer userId;//用户id
    private String bookNo; //商品编号
    private Date robTime;  //抢购时间
}

package com.debug.middleware.model.entity;

import lombok.Data;
import lombok.ToString;

//商品库存实体
@Data
@ToString
public class ProductStock {
    private Integer id;   //主键Id
    private String bookNo;//商品编号
    private Integer stock;//存库
    private Byte isActive;//是否上架
}

mapper

package com.zheng.travel.lock.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zheng.travel.lock.model.ProductRecords;

//商品抢购记录实体
public interface ProductRecordsMapper extends BaseMapper<ProductRecords> {

}

package com.zheng.travel.lock.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zheng.travel.lock.model.ProductRecords;
import com.zheng.travel.lock.model.ProductStock;

//商品抢购记录实体
public interface ProductStockMapper extends BaseMapper<ProductStock> {

}

service

package com.zheng.travel.lock.service.zk;

import com.baomidou.mybatisplus.extension.service.IService;
import com.zheng.travel.vo.ProductVo;

/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 飞哥B站地址:https://space.bilibili.com/490711252
 * 记得关注和三连哦!
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2022/4/12$ 14:44$
 */
public interface IProductService {
    /**
     * 不加锁的商品抢购
     * @param productVo
     */
    void purchaseProductNoLock(ProductVo productVo);
    /**
     * 使用分布式锁解决商品的抢购超卖的问题
     * @param productVo
     */
    void purchaseProductZKLock(ProductVo productVo);
}

serviceimpl

package com.zheng.travel.lock.service.zk;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zheng.travel.lock.mapper.ProductRecordsMapper;
import com.zheng.travel.lock.mapper.ProductStockMapper;
import com.zheng.travel.lock.model.ProductRecords;
import com.zheng.travel.lock.model.ProductStock;
import com.zheng.travel.vo.ProductVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.concurrent.TimeUnit;


@Service
@Slf4j
public class ProductServiceImpl implements IProductService {

    @Autowired
    private ProductStockMapper productStockMapper;
    @Autowired
    private ProductRecordsMapper productRecordsMapper;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void purchaseProductNoLock(ProductVo productVo) {

        // 统计每个用户是否已经抢购该商品了
        Long total = this.countByProductNoUserId(productVo.getUserId(), productVo.getProductNo());
        if (null != total && total > 0) {
            throw new RuntimeException("你已经抢过了!!!");
        }

        // 根据商品编号查询商品库存的信息
        ProductStock productStock = this.selectProductStock(productVo.getProductNo());
        // 判断当前用户是否已经抢购和库存是否充足
        if (null != productStock && productStock.getStock() > 0 && total <= 0) {
            log.info("-----当前商品编号{} ,库存是:{},抢购用户是:{}", productStock.getProductNo(), productStock.getStock(), productVo.getUserId());
            // 抢购成功,进行更新库存
            // 同时把抢购的商品和用户记录保存到ProductRecords
            int updateResponse = this.updateProductStock(productVo.getProductNo());
            // 更新库存成功,需要添加用户的抢购记录,防止用户重复抢购
            if (updateResponse > 0) {
                // 创建抢购记录用户实体对象
                ProductRecords productRecords = new ProductRecords();
                productRecords.setProductNo(productVo.getProductNo());
                productRecords.setUserId(productVo.getUserId());
                productRecords.setCreateTime(new Date());
                // 保存用户抢购记录
                productRecordsMapper.insert(productRecords);
            }
        } else {
            log.info("-----当前商品编号{} ,库存是:{},库存补足", productStock.getProductNo(), productStock.getStock(), productVo.getUserId());
            throw new RuntimeException("商品库存不足!!!");
        }
    }


    @Autowired
    private CuratorFramework client;

    // 临时节点 后面这个/记得加上去. 和下面商品编号和用户链接在一起了
    // /pug_middle_lock/middleware/zklock1000101-lock
    // /pug_middle_lock/middleware/zklock/1000101-lock000001
    private static final String PRODUCT_TEMP_PATH = "/middleware/zklock/";

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void purchaseProductZKLock(ProductVo productVo) {
        // 创建zookeper的互斥锁,A /pug_middle_lock/middleware/zklock/1000101-lock t1 t2 t3 --- 30s
        // 创建zookeper的互斥锁,B /pug_middle_lock/middleware/zklock/1000102-lock t1 t2 t3 --- 30s
        InterProcessMutex mutex = new InterProcessMutex(client, PRODUCT_TEMP_PATH
                + productVo.getProductNo() + productVo.getUserId() + "-lock");
        try {
            //A t1 t2 t3 ---排队取号
            //B t1 t2 t3 ---排队取号 10 500
            if (mutex.acquire(30, TimeUnit.SECONDS)) {
                Long total = this.countByProductNoUserId(productVo.getUserId(), productVo.getProductNo());
                if (null != total && total > 0) {
                    throw new RuntimeException("你已经抢过了!!!");
                }

                // 根据商品编号查询商品库存的信息
                ProductStock productStock = this.selectProductStock(productVo.getProductNo());
                // 判断当前用户是否已经抢购和库存是否充足
                if (null != productStock && productStock.getStock() > 0 && total <= 0) {
                    log.info("-----当前商品编号{} ,库存是:{},抢购用户是:{}", productStock.getProductNo(), productStock.getStock(), productVo.getUserId());
                    // 抢购成功,进行更新库存
                    // 同时把抢购的商品和用户记录保存到ProductRecords
                    int updateResponse = this.updateProductStock(productVo.getProductNo());
                    // 更新库存成功,需要添加用户的抢购记录,防止用户重复抢购
                    if (updateResponse > 0) {
                        // 创建抢购记录用户实体对象
                        ProductRecords productRecords = new ProductRecords();
                        productRecords.setProductNo(productVo.getProductNo());
                        productRecords.setUserId(productVo.getUserId());
                        productRecords.setCreateTime(new Date());
                        // 保存用户抢购记录
                        productRecordsMapper.insert(productRecords);
                    }

                    // 下单发消息 MQ / redis queue /delay queue--webscoket/轮询

                } else {
                    log.info("-----当前商品编号{} ,库存是:{},库存补足", productStock.getProductNo(), productStock.getStock(), productVo.getUserId());
                    throw new RuntimeException("商品库存不足!!!");
                }
            } else {
                log.info("zookeeper 获取锁失败!!!,如果进来的了,说明就不具备可重入性");
                throw new RuntimeException("zookeeper 获取锁失败!!!");
            }
        } catch (Exception ex) {
            throw new RuntimeException("抢购失败..." + ex.getMessage());
        } finally {
            try {
                //进行锁释放
                mutex.release();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }


    /**
     * 根据商品编号查询商品的库存信息
     *
     * @param productNo
     * @return
     */
    private ProductStock selectProductStock(String productNo) {
        LambdaQueryWrapper<ProductStock> productStockLambdaQueryWrapper
                = new LambdaQueryWrapper<>();
        productStockLambdaQueryWrapper.eq(ProductStock::getProductNo, productNo);
        //1:根据商品编号查询库存信息
        ProductStock productStock = productStockMapper.selectOne(productStockLambdaQueryWrapper);
        return productStock;
    }

    /**
     * 根据商品编号和抢购用户信息,判断当前用户是否已经抢购商品
     *
     * @param productNo
     * @return result > 1 ? 已经抢购 :  没有抢购
     */
    private Long countByProductNoUserId(Integer userId, String productNo) {
        LambdaQueryWrapper<ProductRecords> productRecordsLambdaQueryWrapper
                = new LambdaQueryWrapper<>();
        productRecordsLambdaQueryWrapper.eq(ProductRecords::getUserId, userId);
        productRecordsLambdaQueryWrapper.eq(ProductRecords::getProductNo, productNo);
        return this.productRecordsMapper.selectCount(productRecordsLambdaQueryWrapper);
    }


    /**
     * 更新商品的库存-1
     *
     * @return
     */
    private int updateProductStock(String productNo) {
        // 设置更新的值
        UpdateWrapper<ProductStock> updateWrapper = new UpdateWrapper<>();
        // 修改的列
        updateWrapper.setSql("stock = stock-1");
        // 条件
        updateWrapper.eq("product_no", productNo);
        updateWrapper.eq("is_active", 1);
        // 要修改的列
        ProductStock productStock = new ProductStock();
        // 执行更新
        return this.productStockMapper.update(productStock, updateWrapper);
    }


}

productvo

package com.zheng.travel.vo;

import lombok.Data;

import java.io.Serializable;

@Data
public class ProductVo implements Serializable {

    // 抢购的用户, 建议在后端获取
    private Integer userId;
    // 抢购商品的编号
    private String productNo;
    // 抢购商品的id
    //private String productId;
}

controller

package com.zheng.travel.lock.controller.zk;

import com.zheng.travel.lock.common.R;
import com.zheng.travel.lock.common.StatusCode;
import com.zheng.travel.lock.service.zk.IProductService;
import com.zheng.travel.vo.ProductVo;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "zk分布式锁- 商品抢购扣减库存")
public class ProductController {

    @Autowired
    private IProductService productService;


    @GetMapping("/purchase/product")
    public R purchaseProduct(ProductVo productVo) {
        if (StringUtils.isEmpty(productVo.getProductNo())) {
            return new R(701, "商品找不到!!!");
        }

        if (StringUtils.isEmpty(productVo.getUserId())) {
            return new R(701, "找不到用户!!!");
        }

        R response = new R(StatusCode.Success);

        try {
            // 不加锁锁商品抢购
            //productService.purchaseProductNoLock(productVo);

            // zookeeper的分布式锁
            productService.purchaseProductZKLock(productVo);

        }catch ( Exception ex){
            response = new R(StatusCode.Fail.getCode(),ex.getMessage());
        }

        return response;
    }


}

十二、基于Redisson的方式实现分布式锁 -秒杀

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.6</version>
</dependency>
#redisson配置
redisson.host.config=redis://127.0.0.1:6379
package com.debug.middleware.server.config;/**
 * Created by Administrator on 2019/4/27.
 */

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * Redisson相关开源组件自定义注入
 **/
@Configuration
public class RedissonConfig {

    @Autowired
    private Environment env;

    /**
     * 自定义注入配置操作Redisson的客户端实例
     * @return
     */
    @Bean
    public RedissonClient config(){
        //创建配置实例
        Config config=new Config();
        //可以设置传输模式为EPOLL,也可以设置为NIO等等
        //config.setTransportMode(TransportMode.NIO);
        //设置服务节点部署模式:集群模式;单一节点模式;主从模式;哨兵模式等等
        //config.useClusterServers().addNodeAddress(env.getProperty("redisson.host.config"),env.getProperty("redisson.host.config"));
        config.useSingleServer()
                .setAddress(env.getProperty("redisson.host.config"))
                .setKeepAlive(true);
        //创建并返回操作Redisson的客户端实例
        return Redisson.create(config);
    }
}

十三、基于Redisson的方式实现分布式锁 -一次性锁实战

1. 简介

官网:Redisson: Easy Redis Java client and Real-Time Data Platform

快速入门:https://github.com/redisson/redisson#quick-start

文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列

的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List,

Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong,

CountDownLatch,Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live

Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。

Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中

地放在处理业务逻辑上。

2. SpringBoot整合Redisson

依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>

            <artifactId>lettuce-core</artifactId>

        </exclusion>

    </exclusions>

</dependency>

<dependency>
    <groupId>redis.clients</groupId>

    <artifactId>jedis</artifactId>

    <version>3.8.0</version>

</dependency>

<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>

    <artifactId>redisson</artifactId>

    <version>3.17.0</version>

</dependency>

初始化Redisson的客户端连接

#redisson配置
redisson.host.config=redis://120.77.34.190:6379
package com.zheng.travel.lock.config; 
/**
 * Created by Administrator on 2023/4/27.
 */

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * Redisson相关开源组件自定义注入
 **/
@Configuration
public class RedissonConfig {

    @Autowired
    private Environment env;

    /**
     * 自定义注入配置操作Redisson的客户端实例
     *
     * @return
     */
    @Bean
    public RedissonClient config() {
        //创建配置实例
        Config config = new Config();
        //可以设置传输模式为EPOLL,也可以设置为NIO等等
        //config.setTransportMode(TransportMode.NIO);
        //设置服务节点部署模式:集群模式;单一节点模式;主从模式;哨兵模式等等
        //config.useClusterServers().addNodeAddress(env.getProperty("redisson.host.config"),env.getProperty("redisson.host.config"));
        // “时间狗” 自动延长锁执行时间,防止死锁的一种机制
        //config.setLockWatchdogTimeout(30000L);
        
        config.useSingleServer()
                // 设置密码
                .setPassword("mkxiaoer1986.")
                // 你业务选择redis的db库
                .setDatabase(6)
                // redis的单节点地址
                .setAddress(env.getProperty("redisson.host.config"))
                // 保持tcp链接
                .setKeepAlive(true);
        //创建并返回操作Redisson的客户端实例
        return Redisson.create(config);
    }
}

使用redisson的功能–用户注册

 @Autowired
    private RedissonClient redissonClient;

    @Override
    public void regUserRedissionLock(UserRegVo userRegVo) {
        // D yykk_lock 1 ?--晚于 A B C
        // 1: 设置redis的sexnx的key,考虑到幂等性,这个key有如下做法
        // 前提是:前userRegVo.getUserName()必须唯一的,
        // 为什么这样要唯一:A 注册 T1 T2 T3  | B也能够注册 T1 T2 T3 1W A 9999
        // A用户 t1
        String key = "redissolock_" + userRegVo.getUserName();
        // 获redisson分布式锁
        RLock lock = redissonClient.getLock(key);
        try {
            // 访问共享资源前上锁
            // 这里主要通过lock.lock方法进行上锁。
            // 上锁成功,不管何种情况下,10s后会自动释放。
            lock.lock(10, TimeUnit.SECONDS);
            // 根据用户名查询用户实体信息,如果不存在进行注册
            LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>();
            // 根据用户名查询对应的用户信息条件
            userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName());
            // 执行查询语句
            UserReg userReg = this.getOne(userRegLambdaQueryWrapper);
            if (null == userReg) {
                // 这里切记一定要创建一个用户
                userReg = new UserReg();
                // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略
                BeanUtils.copyProperties(userRegVo, userReg);
                // 设置注册时间
                userReg.setCreateTime(new Date());
                // 执行保存和注册用户方法
                this.saveOrUpdate(userReg);
            } else {
                // 如果存在就返回用户信息已经存在
                throw new RuntimeException("用户信息已经注册存在了...");
            }
        } catch (Exception ex) {
            log.error("---获取Redisson的分布式锁失败!---");
            throw ex;
        } finally {
            // ---------------------释放锁--不论成功还是失败都应该把锁释放掉
            // 不管发生任何情况,都一定是自己删除自己的锁
            if (lock != null) {
                //释放锁
                log.error("---获取Redisson的分布式锁释放了---");
                lock.unlock();
                // 在一些严格的场景下,也可以调用强制释放锁
                // lock.forceUnlock();
            }
        }
    }

在传统的Java单体应用一般都是通过JDK自身提供的synchronized关键字,Lock类等工具控制并发线程对共享资源的访问。这种方式在很

长一段时间内确实可以起到很好的保护共享资源的作用,

然而此种方式的服务,系统所在的HOST的jdk也很强的依赖性,如果是在单体架构中是没有问题的,但是在分布式或者集群部署的环境

下,服务,系统都是独立,分开部署的,每个而服务实例将拥有自己的独立的HOST,独立的JDK,而这也导致了传统的,通过JDK自身提

供的工具控制多线程并发访问共享资源显得捉襟见肘了。

因此分布式锁出现,分布式锁,它并不是一种全新的中间件或者组件,而是一种机制,一种实现方式,甚至可以说是一种解决方案,它指

的是在分布式集群部署的环境下,通过锁的机制让多个客户端或者多个服务进程,线程互斥地对共享资源进行访问,从而避免并发带来的

安全,和数据不一致等问题。

3. Redisson的分布式锁

采用redisson实现分布式锁,底层还是使用redis,也是利用redis的原子操作和单线程执行的原理实现分布式锁,

在前面的代码中我们很容易发现,自己定义的redis分布式锁,如果出现了redis节点宕机等情况,而该锁又正好处于被锁住的状态,那么

这个锁很有可能或进入到死锁状态,为了避免这个状况的发生,redisson内部提供了一个监控锁的:“time dog” 看门狗,其作用是在

redisson实例被关闭之前,不断地延长分布式锁的有效期,在默认情况下,看门狗检查锁的超时时间是30s,当然在实际业务场景中我们

可以通过Config.lockWatchDogTimout进行设置。

  • redis 重入问题
  • redis 获取锁(setnx)和关闭原子性lua (redisson 获取 lua + 关闭lua)

除此之外,redisson中间件还为我们提供了很多实际开发过程中的一些考虑,比如:“并发访问共享资源”的情况,主要包括了:“可以重入

锁”,公平锁,联锁,红锁,读写锁,信号量和闭锁等。

不同的分布式锁功能组件实现方式,作用以及适用的应用场景也不一样。接下来着重介绍:redisson的可重入锁来进行说明和展开。

Redisson提供的分布式锁这一功能组件有:“一次性” 与 可重入 两种实现方式,

  • 一次性顾名思义是:指的是当前线程如果可以获取到分布式锁,则成功取之,否则拿不到的线程全部永远失败。也就是:“咸鱼永远不翻身”、
  • 可重入是指:指的是当前线程如果可以获取到分布式锁,则成功取之,拿不到的线程就会等待一定的时间,它们并不会立即失败,而是会等待一定时间,重新获取分布式锁,“咸鱼也有翻身之日”

4. Redisson的一次性锁实战

主要通过RLock的lock方法,它的含义是:在分布式锁的获取过程中,高并发产生的多线程时,如果当前线程获取到分布式锁,其他的线

程就会全部失败,获取不到的线程,就像有一道天然的屏障一样,永远地阻隔了该线程与共享资源的相见。

此种方式适合于那些在同一时刻,而且是在很长时间内仍然只允许一个线程访问共享资源的场景,比如:用户注册,重复提交,抢红包、

提现等业务场景。在开源中间件Redisson中,主要通lock.lock()方法实现。

// 获取锁,一定要执行lock.unlock()才会释放
// 假设A ,B C ,A 拿锁 B,C 不阻塞直接释放调用unlock
lock.lock();

// 获取锁,要么执行lock.unlock()才会释放或者超过了10s自动释放.
// 假设A ,B C ,A 拿锁 B,C 不阻塞直接释放调用unlock
lock.lock(10, TimeUnit.SECONDS);
@Autowired
private RedissonClient redissonClient;

@Override
public void regUserRedissionLock(UserRegVo userRegVo) {
    // D yykk_lock 1 ?--晚于 A B C
    // 1: 设置redis的sexnx的key,考虑到幂等性,这个key有如下做法
    // 前提是:前userRegVo.getUserName()必须唯一的,
    // 为什么这样要唯一:A 注册 B也能够注册
    String key = "redissolock_"+userRegVo.getUserName() ;
    // 获redisson分布式锁
    RLock lock = redissonClient.getLock(key);
    try {

        // 访问共享资源前上锁
        // 这里主要通过lock.lock方法进行上锁。
        // 上锁成功,不管何种情况下,10s后会自动释放。
        lock.lock(10,TimeUnit.SECONDS);

        // 根据用户名查询用户实体信息,如果不存在进行注册
        LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 根据用户名查询对应的用户信息条件
        userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName());
        // 执行查询语句
        UserReg userReg = this.getOne(userRegLambdaQueryWrapper);
        if (null == userReg) {
            // 这里切记一定要创建一个用户
            userReg = new UserReg();
            // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略
            BeanUtils.copyProperties(userRegVo, userReg);
            // 设置注册时间
            userReg.setCreateTime(new Date());
            // 执行保存和注册用户方法
            this.saveOrUpdate(userReg);
        } else {
            // 如果存在就返回用户信息已经存在
            throw new RuntimeException("用户信息已经注册存在了...");
        }
    } catch (Exception ex) {
        log.error("---获取Redisson的分布式锁失败!---");
        throw ex;
    } finally {
        // ---------------------释放锁--不论成功还是失败都应该把锁释放掉
        // 不管发生任何情况,都一定是自己删除自己的锁
        if (lock!=null) {
            //释放锁
            lock.unlock();
            // 在一些严格的场景下,也可以调用强制释放锁
            //
            //
            // lock.forceUnlock();
        }
    }
}

5. Redisson分布式锁之可重入实战

分布式锁的可重入是指:当高并发产生多线程时,==如果当前线程不能获取分布式锁,它并不会立即抛弃,而是会等待一定时间,重新尝

试去获取分布式锁,如果可以获取成功,则执行后续操作共享资源的步骤,如果不能获取到锁而且重试的时间到达了上限,则意味着

该线程将被抛弃。

lock.tryLock(10,seconds) 表示当前线程在某一个时刻如果能获取到锁,则会在10秒之后自动释放,如果不能获取到锁,则会一直处于进入尝试的状态。
// A线程 拿到锁 时间只有10s中,如果10s执行不完,会自动释放
// B线程不会释放,阻塞在位置,但是它最多只能阻塞100s
注册 1w lock 30s 1
下单 1w lock 30s 0.01s 100s 几千单 key 
lock.tryLock(100,10,seconds) ,表示这个上限是100s
直到尝试的实际达到一个上限  尝试加锁,最多等待·100s  上锁以后10s会自动释放。

典型的应用场景就是:商城的高并发抢购商品的业务场景。

众所周知,在商城品台在举办热卖商品的营销活动时,对外一般会选出商品“库存有限” 提醒用户希望可以尽快抢购下单,然后在一般情况

下,该热卖商品的实际库存是:“永远”充足的,所有哪怕是在某一时刻出现了超卖现象,商家也会尽快采购商品发货。将库存补足。简单

地理解就是,商城平台允许当前不同用户并发的线程请求数大于商品当前的库存,越多越好,当然同一个用户的并的多个线程请求除外,

因为这种情况就有点类似于刷单了。故而商城平台的商品抢购流程中,虽然需要保证某一个时刻只能有一个用户对应的一个线程 抢购订

单,但是却允许在某一时刻获取不到锁的其他用户线程重新尝试进行获取。

在采用基于中间件Redis的原子操作的实现分布式锁中,如果需要设置线程的可重入性,一般通过while(true)的方式进行,很明显,此种

方式不但不够优雅,还很有可能会加重应用系统整体的负担。

而在Redisson里,可重入只需要通过:lock.tryLock()方法就可以实现了。

比如:

lock.tryLock(10,seconds) 表示当前线程在某一个时刻如果能获取到锁,则会在10秒之后自动释放,如果不能获取到锁,则会一直处于进入尝试的状态。直到尝试的实际达到一个上限,比如:
lock.tryLock(100,10,seconds) ,表示这个上限是100s
尝试枷锁,最多等待·100s  上锁以后10s会自动释放。
    //定义Redisson的客户端操作实例
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 处理书籍抢购逻辑-加Redisson分布式锁
     * @param dto
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public void robWithRedisson(BookRobDto dto) throws Exception{
        final String lockName="redissonTryLock-"+dto.getBookNo()+"-"+dto.getUserId();
        RLock lock=redissonClient.getLock(lockName);
        try {
            Boolean result=lock.tryLock(100,10,TimeUnit.SECONDS);
            if (result){
                //TODO:真正的核心处理逻辑

                //根据书籍编号查询记录
                BookStock stock=bookStockMapper.selectByBookNo(dto.getBookNo());
                //统计每个用户每本书的抢购数量
                int total=bookRobMapper.countByBookNoUserId(dto.getUserId(),dto.getBookNo());

                //商品记录存在、库存充足,而且用户还没抢购过本书,则代表当前用户可以抢购
                if (stock!=null && stock.getStock()>0 && total<=0){
                    //当前用户抢购到书籍,库存减一
                    int res=bookStockMapper.updateStockWithLock(dto.getBookNo());
                    //如果允许商品超卖-达成饥饿营销的目的,则可以调用下面的方法
                    //int res=bookStockMapper.updateStock(dto.getBookNo());

                    //更新库存成功后,需要添加抢购记录
                    if (res>0){
                        //创建书籍抢购记录实体信息
                        BookRob entity=new BookRob();
                        //将提交的用户抢购请求实体信息中对应的字段取值
                        //复制到新创建的书籍抢购记录实体的相应字段中
                        entity.setBookNo(dto.getBookNo());
                        entity.setUserId(dto.getUserId());
                        //设置抢购时间
                        entity.setRobTime(new Date());
                        //插入用户注册信息
                        bookRobMapper.insertSelective(entity);

                        log.info("---处理书籍抢购逻辑-加Redisson分布式锁---,当前线程成功抢到书籍:{} ",dto);
                    }
                }else {
                    //如果不满足上述的任意一个if条件,则抛出异常
                    throw new Exception("该书籍库存不足!");
                }
            }else{
                throw new Exception("----获取Redisson分布式锁失败!----");
            }
        }catch (Exception e){
            throw e;
        }finally {
            //TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
            if (lock!=null){
                lock.unlock();

                //在某些严格的业务场景下,也可以调用强制释放分布式锁的方法
                //lock.forceUnlock();
            }
        }
    }

十四、基于Redisson的方式的生产者与消费者模式-完成消息的发送和消费

1. 简介

官网:Redisson: Easy Redis Java client and Real-Time Data Platform

快速入门:https://github.com/redisson/redisson#quick-start

文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对

象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque,

BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch,Publish / Subscribe, Bloom filter, Remote service,

Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方

法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑

上。

2. Publish / Subscribe 发布和订阅

生产者与消费模型,事件监听模型其实说的都是一回事,其实前面已经学习MQ方式,那为什么还要学习Redisson这个发布订阅,原因很

简单:性能很高效,而不需要考虑增加额外的学习成本了。

  • 生产者
    • 负责发送消息
  • 消费者
    • 负责监听和消费消息

3. 具体实现

依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>

            <artifactId>lettuce-core</artifactId>

        </exclusion>

    </exclusions>

</dependency>

<dependency>
    <groupId>redis.clients</groupId>

    <artifactId>jedis</artifactId>

    <version>3.8.0</version>

</dependency>

<dependency>
    <groupId>org.redisson</groupId>

    <artifactId>redisson</artifactId>

    <version>3.17.0</version>

</dependency>

配置文件

#redisson配置
redisson.host.config=redis://120.77.34.190:6379

配置类

package com.zheng.travel.lock.config; /**
 * Created by Administrator on 2019/4/27.
 */

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * Redisson相关开源组件自定义注入
 *
 * @Author:debug (yykk)
 * @Date: 2019/4/27 13:34
 **/
@Configuration
public class RedissonConfig {

    @Autowired
    private Environment env;

    /**
     * 自定义注入配置操作Redisson的客户端实例
     *
     * @return
     */
    @Bean
    public RedissonClient config() {
        //创建配置实例
        Config config = new Config();
        //可以设置传输模式为EPOLL,也可以设置为NIO等等
        //config.setTransportMode(TransportMode.NIO);
        //设置服务节点部署模式:集群模式;单一节点模式;主从模式;哨兵模式等等
        //config.useClusterServers().addNodeAddress(env.getProperty("redisson.host.config"),env.getProperty("redisson.host.config"));
        // “时间狗” 自动延长锁执行时间,防止死锁的一种机制
        //config.setLockWatchdogTimeout(30000L);
        config.useSingleServer()
                // 设置密码
                .setPassword("mkxiaoer1986.")
                // 你业务选择redis的db库
                .setDatabase(6)
                // redis的单节点地址
                .setAddress(env.getProperty("redisson.host.config"))
                // 保持tcp链接
                .setKeepAlive(true);
        //创建并返回操作Redisson的客户端实例
        return Redisson.create(config);
    }
}

生产者发送消息

QueuePublisher.java

package com.zheng.travel.lock.controller.redisson;

import com.zheng.travel.lock.common.R;
import com.zheng.travel.lock.common.StatusCode;
import com.zheng.travel.lock.queue.QueuePublisher;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@Api(tags = "基于Redisson - 消息发送")
public class QueueController {

    @Autowired
    private QueuePublisher queuePublisher;


    /**
     * 发送消息
     *
     * @param msg
     * @return
     */
    @GetMapping("/product/send/msg")
    public R sendMessage(String msg) {
        R response = new R(StatusCode.Success);
        try {
            queuePublisher.sendMessage(msg);
        } catch (Exception ex) {
            response = new R(StatusCode.Fail.getCode(), ex.getMessage());
        }
        return response;
    }

}
package com.zheng.travel.lock.queue;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class QueuePublisher {


    @Autowired
    private RedissonClient redissonClient;

    /**
     * 发送消息
     */
    public void sendMessage(String msg) {
        try {
            // 消息存储的队列的名称
            final String queueName = "pug_redisson_msg_queue";
            // 获取队列的实列
            RQueue<Object> queue = redissonClient.getQueue(queueName);
            // 把消息添加对队列中
            queue.add(msg);
            log.info("redisson 队列生产消息-,消息发送成功,内容是:{}", msg);
        } catch (Exception ex) {
            log.error("redisson 队列生产消息-发送失败,出现异常:{}", ex.getMessage());
        }
    }


}

消费者接收消息

package com.zheng.travel.lock.queue;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@Slf4j
public class QueueConsummer implements ApplicationRunner, Ordered {


    @Autowired
    private RedissonClient redissonClient;


    /**
     * 这个方式是指springboot在启动成功之后加载的一个方法
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("消费消息进来了......");
        // 1: 指定你消费的队列
        final String queueName = "pug_redisson_msg_queue";
        // 2: 获取队列的列表信息
        RQueue<String> rQueue = redissonClient.getQueue(queueName);
        // 死循环不停的去监听队列释放存在消息,存在就消费掉
        while (true) {
            String msg = rQueue.poll();
            if (!StringUtils.isEmpty(msg)) {
                log.info("队列消费者监听消息的内容是:{}", msg);
                // 比如:发送es/lostassh/mq/webscoket
            }
        }
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

十五、基于Redis的方式分布式锁 - 用户注册

1. 简介

分布式锁处理可以采用基于数据库级别的:“乐观锁” 和“悲观锁”实现之外,还可以采用业界比较流行的的方

式比如:

Redis的原子操作实现分布式锁,以及Zookeeper的临时节点和Watcher机制实现分布式锁。

  • Redisson (企业级)
  • curator –分布式锁(企业级)

官方文档:Docs

2. Redis的典型应用场景

  • 热点数据的存储和展示,即大部分频繁访问的数据
  • 最近访问的数据存储和展示,即用户访问的最新足迹
  • 消息已读,未读,收藏,浏览数,足迹
  • 好友关注,粉丝,互关,共同好友
  • 限流,黑白名单,抢红包
  • 并发访问控制,分布式锁机制
  • 排行榜:用来代替传统基于数据库的order by
  • 队列机制,基于主题式的发布和订阅,原生的
  • 地理定位GEO
  • 布隆过滤等

3. Redis实现分布式锁 - 幂等性问题

幂等性:

就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用

在Redis中,可以实现分布式锁的原子操作主要是Set和Expire操作。从redis的2.6x开始,提供了set命令如下:

set key value [EX seconds] [PX milliseconds] [NX|XX]

在改命令中,对应key和value,给为应该很熟悉了。

  • EX 是指key的存活时间秒单位
  • PX 是指key的存活时间毫秒单位
  • NX是指当Key不存在的时候才会设置key值
  • XX是指当Key存在时才会设置key的值

从该操作命令不难看出,NX机制其实就是用于实现分布式锁的核心,即所谓的SETNX操作,但是在使用setnx实

现分布式锁需要注意

以下几点:java setIfAbsent === redis-cli setnx

获取锁 1 ,获取不到就是:0

  • 使用setnx命令获取:“锁”时,如果操作结果返回0.(表示key已经对应的锁)已经不存在,即当前已被其他的线程获取了锁,则获取:“锁”失败,反之获取成功
  • 为了防止并发线程在获取锁之后,程序出现异常的情况,从而导致其他线程在调用setnx命令时总是返回1而进入死锁状态,需要为key设置一个“合理”的过期时间==
  • 当成功获取:“锁”并执行完成相应的操作之后,需要释放该“锁”,可以通过执行del命令讲“锁”删除,而在删除的时候还需要保证所删
  • 除的锁,是当前线程所获取的,从而避免出现误删除的情况。

4. 图解

能够实现分布式锁,源自于Redis提供了所有操作的命令均是原子性的,

所谓的:“原子性”指的是一个操作要么全部完成,要么全部不完成,每个操作和命令如同一个整体,不可能进

行分割。

5. 实现用户注册

例:实现用户注册,使用redis分布式锁解决重复注册问题

5.1. 依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.8.0</version>
</dependency>

5.2. 配置

#redis
spring.redis.host=120.77.34.190
spring.redis.port=6379
spring.redis.password=xxxxx
spring.redis.database=15
spring.redis.timeout=5000
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=30
spring.redis.jedis.pool.min-idle=3

5.3. sql脚本

-- ----------------------------
-- Table structure for user_reg
-- ----------------------------
DROP TABLE IF EXISTS `user_reg`;
CREATE TABLE `user_reg` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '密码',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8 COMMENT='用户注册信息表';

-- ----------------------------
-- Records of user_reg
-- ----------------------------
INSERT INTO `user_reg` VALUES ('53', 'linsen', '123456', '2019-04-20 23:01:08');
INSERT INTO `user_reg` VALUES ('54', 'debug', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('55', 'debug', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('56', 'jack', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('57', 'sam', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('58', 'jack', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('59', 'jack', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('60', 'sam', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('61', 'sam', '123456', '2019-04-20 23:36:42');
INSERT INTO `user_reg` VALUES ('62', 'database', '123456', '2019-04-20 23:59:41');
INSERT INTO `user_reg` VALUES ('63', 'rabbitmq', '123456', '2019-04-20 23:59:41');
INSERT INTO `user_reg` VALUES ('64', 'lock', '123456', '2019-04-20 23:59:41');
INSERT INTO `user_reg` VALUES ('65', 'java', '123456', '2019-04-20 23:59:41');
INSERT INTO `user_reg` VALUES ('66', 'redis', '123456', '2019-04-20 23:59:41');
INSERT INTO `user_reg` VALUES ('71', 'luohou', '123456', '2019-04-21 21:51:05');
INSERT INTO `user_reg` VALUES ('72', 'lixiaolong', '123456', '2019-04-21 21:51:05');
INSERT INTO `user_reg` VALUES ('73', 'zhongwenjie', '123456', '2019-04-21 21:51:05');
INSERT INTO `user_reg` VALUES ('74', 'userC', '123456', '2019-05-03 15:37:37');
INSERT INTO `user_reg` VALUES ('75', 'userD', '123456', '2019-05-03 15:37:37');
INSERT INTO `user_reg` VALUES ('76', 'userA', '123456', '2019-05-03 15:37:37');
INSERT INTO `user_reg` VALUES ('77', 'userB', '123456', '2019-05-03 15:37:37');

5.4. 配置类

package com.debug.middleware.server.config;/**
 * Created by Administrator on 2019/3/13.
 */

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 通用化配置
 **/
public class RedisConfiguration {

    //Redis链接工厂
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 缓存redis-redisTemplate
     * @return
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(){
        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //TODO:指定大key序列化策略为为String序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        //TODO:指定hashKey序列化策略为String序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    /**
     * 缓存redis-stringRedisTemplate
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(){
        //采用默认配置即可-后续有自定义配置时则在此处添加即可
        StringRedisTemplate stringRedisTemplate=new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }

}

5.5. bean,mapper

具体看代码

5.6. 实现分布式锁

package com.zheng.travel.lock.service.redis;

import com.baomidou.mybatisplus.extension.service.IService;
import com.zheng.travel.lock.model.UserReg;
import com.zheng.travel.vo.UserRegVo;

public interface IUserRegService extends IService<UserReg> {
    // 无锁
    void regUserNoLock(UserRegVo userRegVo);

    // redis的分布式锁
    void regUserRedisLock(UserRegVo userRegVo);

    //void regUserZkLock(UserRegVo userRegVo);

    //void regUserRedissionLock(UserRegVo userRegVo);
}
package com.zheng.travel.lock.service.redis;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zheng.travel.lock.mapper.UserRegMapper;
import com.zheng.travel.lock.model.UserReg;
import com.zheng.travel.vo.UserRegVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class UserRegServiceImpl extends ServiceImpl<UserRegMapper, UserReg> implements IUserRegService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public void regUserNoLock(UserRegVo userRegVo) {
        // 根据用户名查询用户实体信息,如果不存在进行注册
        LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 根据用户名查询对应的用户信息条件
        userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName());
        // 执行查询语句
        UserReg userReg = this.getOne(userRegLambdaQueryWrapper);

        if (null == userReg) {
            // 这里切记一定要创建一个用户
            userReg = new UserReg();
            // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略
            BeanUtils.copyProperties(userRegVo, userReg);
            // 设置注册时间
            userReg.setCreateTime(new Date());
            // 执行保存和注册用户方法
            this.saveOrUpdate(userReg);
        } else {
            // 如果存在就返回用户信息已经存在
            throw new RuntimeException("用户信息已经注册存在了...");
        }
    }


    @Override
    public void regUserRedisLock(UserRegVo userRegVo) {
        // D yykk_lock 1 ?--晚于 A B C
        // 1: 设置redis的sexnx的key,考虑到幂等性,这个key有如下做法
        // 前提是:前userRegVo.getUserName()必须唯一的,
        // 为什么这样要唯一:A 注册 B也能够注册
        String key = userRegVo.getUserName() + "_lock";
        // 给每个线程线程产生不同的value,目的是:用于删除锁的判断
        String value = System.nanoTime() + "" + UUID.randomUUID();
        // 通过java代码设置setnx
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 获取锁
        //这里又一个隐患,设置太长吞吐量下降,太短可能会造成资源不一致。 这个时间一定要根据实际情况而定
        Boolean resLock = opsForValue.setIfAbsent(key, value,20L,TimeUnit.SECONDS);
        if (resLock) {
            try {
                // 根据用户名查询用户实体信息,如果不存在进行注册
                LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>();
                // 根据用户名查询对应的用户信息条件
                userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName());
                // 执行查询语句
                UserReg userReg = this.getOne(userRegLambdaQueryWrapper);
                if (null == userReg) {
                    // 这里切记一定要创建一个用户
                    userReg = new UserReg();
                    // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略
                    BeanUtils.copyProperties(userRegVo, userReg);
                    // 设置注册时间
                    userReg.setCreateTime(new Date());
                    // 执行保存和注册用户方法
                    this.saveOrUpdate(userReg);
                } else {
                    // 如果存在就返回用户信息已经存在
                    throw new RuntimeException("用户信息已经注册存在了...");
                }
            } catch (Exception ex) {
                throw ex;
            } finally {
                // ---------------------释放锁--不论成功还是失败都应该把锁释放掉
                // 不管发生任何情况,都一定是自己删除自己的锁
                if (opsForValue.get(key).toString().equals(value)) {
                    stringRedisTemplate.delete(key);
                }
            }
        }
    }
}

5.7. controller

package com.zheng.travel.lock.controller.redis;

import com.zheng.travel.lock.common.R;
import com.zheng.travel.lock.common.StatusCode;
import com.zheng.travel.lock.service.redis.IUserRegService;
import com.zheng.travel.vo.UserRegVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@Slf4j
@Api(tags = "Redis分布式锁--用户注册")
public class UserRegController {

    @Autowired
    private IUserRegService userRegService;


    @PostMapping("/user/reg/uuid")
    public String reg() {
        return UUID.randomUUID().toString();
    }


    @ApiOperation("用户注册")
    @GetMapping("/user/reg")
    public R reguser(UserRegVo userRegVo) {
        if (StringUtils.isEmpty(userRegVo.getUserName())) {
            return new R(701, "请输入用户...");
        }
        if (StringUtils.isEmpty(userRegVo.getPassword())) {
            return new R(701, "请输入密码...");
        }

        // 进行用户注册
        R response = new R(StatusCode.Success);
        try {
            // 无锁版本
            //userRegService.regUserNoLock(userRegVo);

            // redis分布式锁
            userRegService.regUserRedisLock(userRegVo);

        } catch (Exception ex) {
            response = new R(701, "注册用户失败");
        }

        return response;
    }

}

6. 小结

  • Redis的分布式锁主要采用setnx+ expire命令来完成
  • Redis的分布式锁得以实现,主要是得益于Redis的单线程操作机制,即在底层基础架构中,同一时

刻,同一个部署节点只允许一个线程执行某种操作,这种操作也称之为原子性。

  • 上面的用户注册重复提交的问题,使用了分布式锁的一次性锁,即同一时刻并发的线程锁携带的相

同数据只能允许一个线程通过,其他的线程将获取锁失败,而从结束自身的业务流程。

十六、基于Zookeeper的方式实现分布式锁 - 用户注册

案例场景:

  • 商品表
  • 商品库存表
  • 抢购商品下单
  • Redis有一个Queue—下单发消息/ delayQueue

1. 简介

Zookeeper是一款开源的分布式服务协调中间件,是由雅虎团队研发而来,其设计的初衷是开发一个通

用的,无单点问题的分布式协调框架,采用统一的协调管理方式更好地管理各个子系统,从而让开发者

将更多的经理集中在业务逻辑处理上。最终整个分布式系统看上去就想是一个大型的动物园,而这个中

间件正好用来协调分布式环境中的各个子系统,zookeeper因此而得名

2. Zookeeper可以用来干嘛?

官网:Apache ZooKeeper

  • 统一配置管理:将每个子系统都需要配置的文件统一放到zookeeper中的znode节点中。
  • 统一命名服务:通过给存放在znode上的资源进行统一命名,各个子系统便可以通过名字获取到节

点上响应的资源。

  • 分布式锁:通过创建于该共享资源相关的”顺序临时节点“与动态watcher监听机制,而从监控多线

程对共享资源的并发访问。(排队取号的机制,如果不明白可以百度找一些文章,其实更加建议结合

redis去理解会更好)

    • 为什么一定是:顺序临时节点,因为可以保证每个线程执行的都是唯一的。
    • watcher机制机制:把最小的序号删除以后,一定要把List第一个元素给删除,让其他的线程继

续获取锁。

  • 集群状态:通过动态地感知节点的增加,删除,从而保证集群下的相关节点数据主,副本数据的一

致。

3. Zookeeper分布式锁流程

zookeeper实现分布式锁主要是通过创建与共享资源相关的:

“顺序临时节点” 并采用其提供的Watcher监听机制,控制多线程对共享资源的并发访问,整体如下:

4. SpringBoot整合Zookeeper

下载地址:Index of /dist/zookeeper

依赖

<!--zookeeper-->
<dependency>
    <groupId>org.apache.zookeeper</groupId>

    <artifactId>zookeeper</artifactId>

    <version>3.6.3</version>

    <exclusions>
        <exclusion>
            <artifactId>slf4j-log4j12</artifactId>

            <groupId>org.slf4j</groupId>

        </exclusion>

    </exclusions>

</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
    <groupId>org.apache.curator</groupId>

    <artifactId>curator-framework</artifactId>

    <version>4.3.0</version>

</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>

    <artifactId>curator-recipes</artifactId>

    <version>4.3.0</version>

</dependency>

配置

#zookeeper配置
zk.host=127.0.0.1:2181
zk.namespace=pug_middle_lock

配置初始化

package com.zheng.travel.lock.config; /**
 * Created by Administrator on 2019/3/13.
 */

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 通用化配置
 *
 * @Date: 2019/3/13 8:38
 * @Link:微信-debug0868 QQ-1948831260
 **/
@Configuration
public class CuratorFrameworkConfiguration {

    //读取环境变量的实例
    //@Autowired
    //private Environment env;
    @Value("${zk.host}")
    private String host;
    @Value("${zk.namespace}")
    private String namespace;


    //自定义注入Bean-ZooKeeper高度封装过的客户端Curator实例
    @Bean
    public CuratorFramework curatorFramework() {
        //创建CuratorFramework实例
        //(1)创建的方式是采用工厂模式进行创建;
        //(2)指定了客户端连接到ZooKeeper服务端的策略:这里是采用重试的机制(5次,每次间隔1s)
//        CuratorFramework curatorFramework= CuratorFrameworkFactory.builder()
//                .connectString(env.getProperty("zk.host")).namespace(env.getProperty("zk.namespace"))
//                .retryPolicy(new RetryNTimes(5,1000)).build();
        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(host).namespace(namespace)
                .retryPolicy(new RetryNTimes(5, 1000)).build();
        curatorFramework.start();
        //返回CuratorFramework实例
        return curatorFramework;
    }

}

用户注册实现分布式锁

package com.debug.middleware.server.service.lock;/**
 * Created by Administrator on 2019/4/20.
 */

import com.debug.middleware.model.entity.UserReg;
import com.debug.middleware.model.mapper.UserRegMapper;
import com.debug.middleware.server.controller.lock.dto.UserRegDto;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 处理用户注册信息提交服务Service
 **/
@Service
public class UserRegService {
    //定义日志实例
    private static final Logger log= LoggerFactory.getLogger(UserRegService.class);
    //定义用户注册Mapper操作接口实例
    @Autowired
    private UserRegMapper userRegMapper;


    //定义ZooKeeper客户端CuratorFramework实例
    @Autowired
    private CuratorFramework client;
    //ZooKeeper分布式锁的实现原理是由ZNode节点的创建与删除跟监听机制构成的
    //而ZNoe节点将对应一个具体的路径-跟Unix文件夹路径类似-需要以 / 开头
    private static final String pathPrefix="/middleware/zkLock/";

    /**
     * 处理用户提交注册的请求-加ZooKeeper分布式锁
     * @param dto
     * @throws Exception
     */
    public void userRegWithZKLock(UserRegDto dto) throws Exception{
        //创建ZooKeeper互斥锁组件实例,需要将监控用的客户端实例、精心构造的共享资源 作为构造参数
        InterProcessMutex mutex=new InterProcessMutex(client,pathPrefix+dto.getUserName()+"-lock");
        try {
            //采用互斥锁组件尝试获取分布式锁-其中尝试的最大时间在这里设置为10s
            //当然,具体的情况需要根据实际的业务而定
            if (mutex.acquire(10L, TimeUnit.SECONDS)){
                //TODO:真正的核心处理逻辑

                //根据用户名查询用户实体信息
                UserReg reg=userRegMapper.selectByUserName(dto.getUserName());
                //如果当前用户名还未被注册,则将当前用户信息注册入数据库中
                if (reg==null){
                    log.info("---加了ZooKeeper分布式锁---,当前用户名为:{} ",dto.getUserName());
                    //创建用户注册实体信息
                    UserReg entity=new UserReg();
                    //将提交的用户注册请求实体信息中对应的字段取值
                    //复制到新创建的用户注册实体的相应字段中
                    BeanUtils.copyProperties(dto,entity);
                    //设置注册时间
                    entity.setCreateTime(new Date());
                    //插入用户注册信息
                    userRegMapper.insertSelective(entity);

                }else {
                    //如果用户名已被注册,则抛出异常
                    throw new Exception("用户信息已经存在!");
                }
            }else{
                throw new RuntimeException("获取ZooKeeper分布式锁失败!");
            }
        }catch (Exception e){
            throw e;
        }finally {
            //TODO:不管发生何种情况,在处理完核心业务逻辑之后,需要释放该分布式锁
            mutex.release();
        }
    }

}

十七、微服务中分布式锁实现与原理分析

  • 了解java中的锁
  • 了解为什么要使用分布式锁,及实现方案
  • 学会使用Redis/ZK来实现分布式锁
  • RLock的分布式锁
  • 分布式锁解决商品超卖

1. 多线程并发引发数据超卖现象

目标

掌握lock和synchorized的应用

场景分析

案例代码

比如:火车站的售票窗口还有100张票,这个时候三个黄牛同时在抢票,或者N个用户在抢票。

看会发生什么问题?

package com.mzy.lock;

public class SellTicket implements Runnable{

    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            if(ticket > 0 ) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
            }
        }
    }
    
}

测试用例

package com.zheng.lock2;

public class SellTicketTest  {


    public static void main(String[] args){

        // 买票测试
        SellTicket sellTicket = new SellTicket();
        for (int i = 1  ; i <=3 ; i++) {
            new Thread(sellTicket,"窗口-"+i).start();
        }
    }

}

结果

Thread-0正在出售第:100张票!
Thread-1正在出售第:99张票!
Thread-2正在出售第:98张票!
Thread-2正在出售第:97张票!
Thread-0正在出售第:95张票!
Thread-1正在出售第:96张票!
Thread-2正在出售第:94张票!
Thread-1正在出售第:93张票!
Thread-0正在出售第:92张票!
Thread-2正在出售第:91张票!
Thread-1正在出售第:90张票!
Thread-0正在出售第:90张票!
Thread-2正在出售第:89张票!
Thread-0正在出售第:88张票!
Thread-1正在出售第:89张票!
Thread-2正在出售第:87张票!
Thread-1正在出售第:85张票!
Thread-0正在出售第:86张票!
Thread-1正在出售第:84张票!
Thread-0正在出售第:83张票!
Thread-2正在出售第:84张票!
Thread-2正在出售第:82张票!
Thread-1正在出售第:81张票!
Thread-0正在出售第:82张票!
Thread-1正在出售第:80张票!
Thread-2正在出售第:80张票!
Thread-0正在出售第:80张票!
Thread-1正在出售第:79张票!
Thread-0正在出售第:78张票!
Thread-2正在出售第:79张票!
Thread-1正在出售第:77张票!
Thread-0正在出售第:76张票!
Thread-2正在出售第:76张票!
Thread-2正在出售第:75张票!
Thread-0正在出售第:74张票!
Thread-1正在出售第:73张票!
Thread-2正在出售第:72张票!
Thread-0正在出售第:72张票!
Thread-1正在出售第:72张票!
Thread-2正在出售第:71张票!
Thread-0正在出售第:71张票!
Thread-1正在出售第:71张票!
Thread-2正在出售第:70张票!
Thread-0正在出售第:70张票!
Thread-1正在出售第:70张票!
Thread-1正在出售第:69张票!
Thread-0正在出售第:68张票!
Thread-2正在出售第:67张票!
Thread-1正在出售第:66张票!
Thread-0正在出售第:65张票!
Thread-2正在出售第:65张票!
Thread-2正在出售第:64张票!
Thread-1正在出售第:63张票!
Thread-0正在出售第:63张票!
Thread-2正在出售第:62张票!
Thread-1正在出售第:62张票!
Thread-0正在出售第:62张票!
Thread-2正在出售第:61张票!
Thread-0正在出售第:60张票!
Thread-1正在出售第:59张票!
Thread-0正在出售第:58张票!
Thread-2正在出售第:56张票!
Thread-1正在出售第:57张票!
Thread-0正在出售第:55张票!
Thread-2正在出售第:55张票!
Thread-1正在出售第:55张票!
Thread-1正在出售第:54张票!
Thread-0正在出售第:52张票!
Thread-2正在出售第:53张票!
Thread-2正在出售第:51张票!
Thread-1正在出售第:50张票!
Thread-0正在出售第:50张票!
Thread-0正在出售第:49张票!
Thread-2正在出售第:48张票!
Thread-1正在出售第:48张票!
Thread-0正在出售第:47张票!
Thread-2正在出售第:45张票!
Thread-1正在出售第:46张票!
Thread-1正在出售第:44张票!
Thread-0正在出售第:43张票!
Thread-2正在出售第:43张票!
Thread-2正在出售第:42张票!
Thread-0正在出售第:41张票!
Thread-1正在出售第:41张票!
Thread-1正在出售第:40张票!
Thread-2正在出售第:39张票!
Thread-0正在出售第:38张票!
Thread-2正在出售第:37张票!
Thread-1正在出售第:37张票!
Thread-0正在出售第:37张票!
Thread-2正在出售第:36张票!
Thread-1正在出售第:36张票!
Thread-0正在出售第:36张票!
Thread-1正在出售第:35张票!
Thread-0正在出售第:34张票!
Thread-2正在出售第:34张票!
Thread-1正在出售第:33张票!
Thread-0正在出售第:31张票!
Thread-2正在出售第:32张票!
Thread-0正在出售第:30张票!
Thread-1正在出售第:28张票!
Thread-2正在出售第:29张票!
Thread-1正在出售第:27张票!
Thread-2正在出售第:25张票!
Thread-0正在出售第:26张票!
Thread-1正在出售第:24张票!
Thread-2正在出售第:23张票!
Thread-0正在出售第:23张票!
Thread-2正在出售第:22张票!
Thread-0正在出售第:21张票!
Thread-1正在出售第:22张票!
Thread-0正在出售第:20张票!
Thread-2正在出售第:18张票!
Thread-1正在出售第:19张票!
Thread-0正在出售第:17张票!
Thread-1正在出售第:16张票!
Thread-2正在出售第:16张票!
Thread-2正在出售第:15张票!
Thread-1正在出售第:14张票!
Thread-0正在出售第:14张票!
Thread-1正在出售第:13张票!
Thread-0正在出售第:11张票!
Thread-2正在出售第:12张票!
Thread-0正在出售第:10张票!
Thread-2正在出售第:8张票!
Thread-1正在出售第:9张票!
Thread-1正在出售第:7张票!
Thread-0正在出售第:6张票!
Thread-2正在出售第:6张票!
Thread-0正在出售第:5张票!
Thread-1正在出售第:5张票!
Thread-2正在出售第:5张票!
Thread-0正在出售第:4张票!
Thread-1正在出售第:2张票!
Thread-2正在出售第:3张票!
Thread-1正在出售第:1张票!
Thread-0正在出售第:-1张票!
Thread-2正在出售第:0张票!

Process finished with exit code 0

小结

出现了超买的的现象,如果解决这个问题呢?用锁来解决

线程具有重入性(执行完毕以后才可以继续争抢cpu资源继续处理),无序性(谁先抢到CPU就谁执

行),不会阻塞。

面试题:线程的 A,B,C模式

2. 解决多线程并发引发数据超卖现象

目标

掌握synchronized的使用方法和存在的问题。

分析

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的

synchronized方法或者访问synchronize代码快时,这个线程便获得了该对象的锁,其他线程暂时无法

访问这个方法,只有等待这个方法执行完毕或者代码快执行完毕,这个线程才会释放该对象的锁,其他

线程才能执行这个方法或者代码块。

理解

加了synchroized的方法和代码块,线程在执行这个方法的时候就会阻塞。

有性能的损耗

排他性(互斥性 ),重入性。

代码

package com.mzy.lock;

public class SellTicket_Sync implements Runnable{
    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            synchronized (this) {
                if(ticket > 0 ) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                }
            }
        }
    }
}

测试

package com.mzy.lock;


public class SellTicketTest {

    public static void main(String[] args) {

        //使用Synchronized锁来解决线程安全问题		SellTicket_Sync sellTicket = new SellTicket_Sync();
        
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

小结

使用 synchronized 的方式解决商品的超卖问题,系统级别的锁,只能jvm去维护,如果你单一的

synchronized 修饰方法和代码块的

候是不会出现死锁的问题。并且它无法去控制和销毁完全都是 jvm 去控制

3. 解决多线程并发引发数据超卖现象 - lock

目标

掌握lock的使用方法和存在的问题、

java.util.concurrent.简称: J.U.C

分析

Lock(轻量级)和synchronized同步块一样,是一种线程同步机制,

但比java的synchronized同步块更复杂。

自Java5开始,在java.util.concurrent.locks包中包含了一些锁的实现,它们可以帮助我们解决进程内多

线程并发时的数据一致性问题。

Lock是一个接口,里面的方法有:

  • lock() 获取锁,如果锁被占用,其他的线程全部等待。(其他的线程全部阻塞,排它性
  • tryLock() 如果获取锁的时候,锁被占用就返回false,否则返回true (获取到锁的就执行,获取不到的就等待)
  • tryLock(long time,TimeUnit unit) 如果获取锁的时候,锁被占用就返回false,否则返回true 并且释放时间
  • unLock() :释放锁(如果不锁就会出现死锁)
  • lockInterruptibly(); 用该锁的获得方式,如果线程在获取锁的阶段进入等待,那么可以中断次线程,先去做别的事情。

代码

package com.mzy.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * 模拟多线程环境下资源竞争的问题
 */
public class SellTicket_Lock implements Runnable{
    
    //定义锁
    private Lock lock = new ReentrantLock();
    //票数
    private int ticket = 100;
    
    @Override
    public void run() {
        while(ticket > 0 ) {
            lock.lock();//加锁
            try {
                if(ticket > 0 ) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                }
                
            } finally {
                lock.unlock();//一定要释放锁
            }
        }
    }
}

测试

package com.mzy.lock;

public class SellTicketTest {

    public static void main(String[] args) {
        SellTicket_Lock  sellTicket = new SellTicket_Lock();
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

小结

到底哪个好?

  • 两者在某种程度上来来说,没有太大的区别了。在后续oracle公司已经对sync进行优化了,性能几乎和lock差不多
  • 所以不用纠结到底用lock还是sync,但是推荐大家使用:Lock
  • 自己命运能够自己掌握,就绝不交给别人,所以使用Lock

sysnchoized和lock的区别:

问题

使用lock和sync真的安全吗?

  • 在单机环境下是安全的,
  • 在集群环境下,是不安全所以需要使用分布式锁来解决或者数据库级别的锁来解决。

4. 分布式锁级实现方案和特点

何谓分布式锁

在分布式系统中,常常 需要去协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了

一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况

下,便需要使用到分布式锁。

分布式锁是:控制分布式系统之间同步访问共享资源的一种方式

分布式锁架构

在实际应用开发过程中:项目都是集群部署的,采用的机制是都是用nginx+tomcat来完成集群部署,这

个时候每一个服务器都是独立的jvm环境。分布式锁解决方案如下:

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的

动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时

候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

我们来假设一个最简单的:秒杀场景:数据库里有一张表,column分别是商品ID,和商品ID对应的库存

量,秒杀成功就将此商品库存量-1。

现在假设有1000个线程来秒杀两件商品,500个线程秒杀第一个商品,500个线程秒杀第二个商品。我们

来根据这个简单的业务场景来解释一下分布式锁。

通常具有秒杀场景的业务系统都比较复杂,承载的业务量非常巨大,并发量也很高。这样的系统往往采

用分布式的架构来均衡负载。那么这1000个并发就会是从不同的地方过来,商品库存就是共享的资源,

也是这1000个并发争抢的资源,这个时候我们需要将并发互斥管理起来。这就是分布式锁的应用。

分布式锁需要具备哪些条件

  • 互斥性:和我们本地锁一样互斥性是最基本的,但是分布式锁需要保证在不同节点的不同线程互斥。 setnx
  • 可重入性:同一个节点上的同一个线程如果获取了锁之后,那么也可以再次获取这个锁。setnx(key,value)
  • 锁超时:和本地锁一样支持锁超时,防止死锁 expire
  • 支持阻塞和非阻塞:和ReentrantLock一样支持Lock和tryLock以及tryLock(long timeout) redis本身就是单线程
  • 高效:高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  • 支持公平锁和非公平锁:公平锁的意思是按照请求加锁的顺序获取锁,非公平锁就相反是无序的,这个一般来说实现的比较少。

应用场景

  • 秒杀
  • 抢购
  • 抢红包
  • 抽奖

这些场景都有共同的特征就是:资源少,请求大,除了用分不锁来解决以外,还必须加:限流才能够稳

定的处理。

分布式的几个话题

  • 分布式锁(redis/zookeeper)
  • 分布式事务(消息队列/XTA/2PC/3PC/seata/tcc)
  • 分布式订单编号(雪花算法,redis,zk)
  • 分布式会话(redis+cookie/ shiro+cas/jwt/自主研发)
  • 限流(信号量,guava、nginx限流)
  • 缓存(redis/memcacha/ehcache/tair)
  • 消息队列(rabbitmq/activemq/rocketmq)
  • 提升网站的性能(异步编程async,fork/join合并请求,Disxxxx)

实现分布式锁的几种方案

  • 文件系统
  • 数据库实现主键,唯一约束 for update(乐观锁)(写入的并发的太大)(lock-table id 1)删掉数据
  • 基于zookeeper的实现,类似文件系统== + countdownlatch
  • 基于redis的实现(推荐) setnx+expire | lua
  • 自研发分布式锁,如:谷歌的chubby。

小结

如果在单机环境(一个tomcat),这个时候你直接用lock和synchorized解决问题了。

但是如果是分布式集群环境,就需要分布式锁来解决问题。

5. 分布式锁Redisson

5.1. 何为分布式锁Redisson

Redisson 的分布式可重入锁RLock.java对象实现了java.util.concurrent.Lock的接口,支持分布式锁。

下面是Redis官网提供的java组件。

5.2. 实现步骤

导入依赖
 <dependency>
     <groupId>org.redisson</groupId>

     <artifactId>redisson</artifactId>

     <version>3.9.1</version>

 </dependency>
初始化RLock
package com.zheng.zhengdistributelock.redis;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedssionLock {

    /*
    *  导入redission的包
    *  <dependency>
         <groupId>org.redisson</groupId>

         <artifactId>redisson</artifactId>

         <version>3.9.1</version>

     </dependency>

    * */


    //zookeeper 分布锁

    public static RLock getLock() {
        //定义一个配置,在redssion有类RLock,实现分布式锁
        Config config = new Config();
        //指定使用单节点部署方式
        //单机
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
                 //config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("mkxiaoer");

        //主从
        /*config.useMasterSlaveServers()
                .setMasterAddress("127.0.0.1:6379")
                .addSlaveAddress("127.0.0.1:6389", "127.0.0.1:6332", "127.0.0.1:6419")
                .addSlaveAddress("127.0.0.1:6399");*/

        //哨兵
        /*config.useSentinelServers()
                .setMasterName("mymaster")
                .addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
                .addSentinelAddress("127.0.0.1:26319");*/

        //集群
       /* config.useClusterServers()
                .setScanInterval(2000) // cluster state scan interval in milliseconds
                .addNodeAddress("127.0.0.1:7000", "127.0.0.1:7001")
                .addNodeAddress("127.0.0.1:7002");*/

        config.useSingleServer().setConnectionPoolSize(500);//设置对于master节点的连接池中连接数最大为500
        config.useSingleServer().setIdleConnectionTimeout(10000);//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
        config.useSingleServer().setConnectTimeout(30000);//同任何节点建立连接时的等待超时。时间单位是毫秒。
        config.useSingleServer().setTimeout(3000);//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
        config.useSingleServer().setPingTimeout(30000);


        //获取RedissonClient对象
        RedissonClient redisson = Redisson.create(config);
        //获取锁对象
        RLock rLock = redisson.getLock("lock.lock");
        return rLock;
    }

}
业务执行
package com.mzy.lock;

import org.redisson.api.RLock;

import com.mzy.redisson.RedssionLock;

/**
 * 
 * 模拟多线程环境下资源竞争的问题
 */
public class SellTicket_RedissonLock implements Runnable{
    
        //定义锁
        private RLock lock =  RedssionLock.getLock();
        //票数
        private int ticket = 100;
        
        @Override
        public void run() {
            while(ticket > 0 ) {
                lock.lock();
                try {
                    if(ticket > 0 ) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                    }
                    
                } finally {
                    lock.unlock();//一定要释放锁
                }
            }
        }
}
测试类
package com.mzy.lock;

public class SellTicketTest {

    public static void main(String[] args) {

        //SellTicket sellTicket = new SellTicket();
        //使用Synchronized锁来解决线程安全问题
        SellTicket_Sync sellTicket = new SellTicket_Sync();
        //使用Lock来保证线程安装 juc--Future
        //TOMCAT 小---catalina-nio-thread-01---spring-/miaosha
        //TOMCAT fly---catalina-nio-thread-02---spring-/miaosha
        //TOMCAT ---catalina-nio-thread-03---spring-/miaosha
        //TOMCAT ---catalina-nio-thread-04---spring-/miaosha
        //TOMCAT ---catalina-nio-thread-05---spring-/miaosha
        //SellTicket_Lock  sellTicket = new SellTicket_Lock();
        //SellTicket_RedisLock  sellTicket = new SellTicket_RedisLock();
        //SellTicket_ZookeeperLock  sellTicket = new SellTicket_ZookeeperLock();
        //SellTicket_RedissonLock sellTicket = new SellTicket_RedissonLock();
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

5.3 小结

6. 自定义分布式锁Redis

目标

自定义redis的分布式锁

分析

基本锁:

原理:利用redis的setnx,如果不存在某个key则设置值,设置成功则表示取得锁成功。setnx(key) 1 代表已经有锁。

缺点:如果获取锁后的过程中 如果业务没用执行完就挂了,则锁永远不会释放。---死锁

改进型:

改进:在基本锁的setnx基础上设置expire,保证超时后也能自动释放锁。

缺点:setnx与expire不是一个原子性,可能执行完setnx该进程就挂了,就没办法超时了。

在改进

改进:利用lua脚本,将setnx于expire变成一个原子操作,可解决一部分问题。

缺点:还是锁过期的问题。

步骤

1:导入redis依赖

2:初始化jedis

3:定义分布式锁类

4:使用分布式锁完成业务的对接

5:进行测试

代码

导入依赖
<dependency>
    <groupId>redis.clients</groupId>

    <artifactId>jedis</artifactId>

</dependency>
初始化jedis及配置信息
redis.ip=127.0.0.1
redis.port=6379
redis.password=
redis.max.total=20
redis.max.idle=10
redis.min.idle=2
redis.test.borrow=true
redis.test.return=false
解析properties的工具类
package com.mzy.util;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;

public class PropertiesUtil {


    private static Properties props;

    static {
        String fileName = "redis.properties";
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
        }
    }

    public static String getProperty(String key){
        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            return null;
        }
        return value.trim();
    }

    public static String getProperty(String key,String defaultValue){

        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            value = defaultValue;
        }        return value.trim();
    }

}
package com.mzy.util;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Created by mofeng
 */
public class RedisPool {
    private static JedisPool pool;//jedis连接池
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total","20")); //最大连接数
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","20"));//在jedispool中最大的idle状态(空闲的)的jedis实例的个数
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","20"));//在jedispool中最小的idle状态(空闲的)的jedis实例的个数

    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的。
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return","true"));//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。

    private static String redisIp = PropertiesUtil.getProperty("redis.ip");
    private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"));
    private static String password = PropertiesUtil.getProperty("redis.password");


    private static void initPool(){
        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);

        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);

        config.setBlockWhenExhausted(true);//连接耗尽的时候,是否阻塞,false会抛出异常,true阻塞直到超时。默认为true。

        pool = new JedisPool(config,redisIp,redisPort,1000*2,password);
    }

    static{
        initPool();
    }

    public static Jedis getJedis(){
        return pool.getResource();
    }


    public static void returnBrokenResource(Jedis jedis){
        jedis.close();
    }



    public static void returnResource(Jedis jedis){
        jedis.close();
    }


    public static void main(String[] args) {
        Jedis jedis = pool.getResource();
        jedis.set("username","zhangsan");
        returnResource(jedis);
        pool.destroy();//临时调用,销毁连接池中的所有连接
        System.out.println("program is end");
    }



}
分布式锁类
package com.mzy.lock;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import com.mzy.util.RedisPool;

import org.apache.commons.io.FileUtils;
import org.springframework.util.ResourceUtils;
import redis.clients.jedis.Jedis;

public class RedisDistributeLock implements Lock {

    
    private final Long RELEASE_SUCCESS = 1L;
    //获取锁时,睡眠等待5毫秒中
    private long SLEEP_PER = 5;
    private final String key = "lock.key"; 
    private final String value = "lock.value"; 
    private final int expireTime = 5 * 1000; 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    //setnx 版本
    private boolean tryGetDistributeLock2(Jedis jedis,String key ,String value) {
        String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    //lua版本
    private boolean tryGetDistributeLock(Jedis jedis,String key ,String value) {
        try {
            String luescript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then  return redis.call('expire',KEYS[1],ARGV[2])  else return 0 end";
            List<String> keys = new ArrayList<>();
            keys.add(key);
            List<String> argv = new ArrayList<>();
            argv.add(value);
            argv.add("5");
            Object result = jedis.eval(luescript,keys,argv);
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    
    public boolean tryLock() {
        try(Jedis jedis = RedisPool.getJedis();){
            return tryGetDistributeLock(jedis, key, value);
        }
    }
    
    public void lock() {
        try(Jedis jedis = RedisPool.getJedis();){
             while(!tryGetDistributeLock(jedis, key, value)) {
                try {
                    Thread.sleep(SLEEP_PER);
                } catch (Exception e) {
                }
             }
        }
    }
    
    private boolean unLock(Jedis jedis,String key ,String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
    
    public void unlock() {
        try(Jedis jedis = RedisPool.getJedis();){
            unLock(jedis, key, value);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
定义业务执行类
package com.mzy.lock;

/**
 * 
 * 模拟多线程环境下资源竞争的问题
 */
public class SellTicket_RedisLock implements Runnable{
    
        //定义锁
        private RedisDistributeLock lock =  new RedisDistributeLock();
        //票数
        private int ticket = 100;
        
        @Override
        public void run() {
            while(ticket > 0 ) {
                lock.lock();
                try {
                    if(ticket > 0 ) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"正在出售第:"+ticket--+"张票!");
                    }
                    
                } finally {
                    lock.unlock();//一定要释放锁
                }
            }
        }
}
测试用例
package com.mzy.lock;

public class SellTicketTest {

    public static void main(String[] args) {

        SellTicket_RedisLock  sellTicket = new SellTicket_RedisLock();
    
        //模拟三个窗口售票
        for (int i = 1; i <= 3; i++) {
            new Thread(sellTicket).start();
        }
    }
}

小结

7. 商品抢购结合分布式锁案例分析

目标

使用纷纷不是锁解决商品超卖问题

代码

springboot创建一个ssm工程
初始化数据库脚本
/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 60011
Source Host           : localhost:3306
Source Database       : test

Target Server Type    : MYSQL
Target Server Version : 60011
File Encoding         : 65001

Date: 2019-11-20 15:15:30
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tb_goods
-- ----------------------------
DROP TABLE IF EXISTS `tb_goods`;
CREATE TABLE `tb_goods` (
  `goods_code` varchar(255) DEFAULT NULL,
  `goods_num` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_goods
-- ----------------------------
INSERT INTO `tb_goods` VALUES ('banala', '234');
INSERT INTO `tb_goods` VALUES ('dress', '356789');
INSERT INTO `tb_goods` VALUES ('shirt', '2334');
INSERT INTO `tb_goods` VALUES ('apple', '0');
相关依赖包的引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>2.2.1.RELEASE</version>

        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.zheng</groupId>

    <artifactId>zheng-distributelock</artifactId>

    <version>0.0.1-SNAPSHOT</version>

    <name>zheng-distributelock</name>

    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>

    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-jdbc</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-data-redis</artifactId>

        </dependency>

        <dependency>
            <groupId>io.lettuce</groupId>

            <artifactId>lettuce-core</artifactId>

        </dependency>

        <dependency>
            <groupId>mysql</groupId>

            <artifactId>mysql-connector-java</artifactId>

            <scope>runtime</scope>

            <version>5.1.10</version>

        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>

            <artifactId>lombok</artifactId>

            <optional>true</optional>

        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>

            <artifactId>zookeeper</artifactId>

            <version>3.4.14</version>

        </dependency>

        <dependency>
            <groupId>com.101tec</groupId>

            <artifactId>zkclient</artifactId>

            <version>0.10</version>

        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>

            <artifactId>redisson</artifactId>

            <version>3.11.2</version>

        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>

            <artifactId>jedis</artifactId>

            <version>2.10.2</version>

        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>

            <artifactId>commons-pool2</artifactId>

            <version>2.6.2</version>

        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>

            <artifactId>druid-spring-boot-starter</artifactId>

            <version>1.1.18</version>

        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>

            <artifactId>fastjson</artifactId>

            <version>1.2.38</version>

        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>

            <artifactId>commons-lang3</artifactId>

            <version>3.6</version>

        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>

            <artifactId>curator-framework</artifactId>

            <version>4.2.0</version>

        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>

            <artifactId>curator-recipes</artifactId>

            <version>4.2.0</version>

        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

            </plugin>

        </plugins>

    </build>

</project>
配置连接信息
server.port=9898
#druid
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
jdbctemplate完成对商品的库存查询和扣减定义和实现
package com.zheng.zhengdistributelock.dao;

import com.zheng.zhengdistributelock.core.BuyException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class GoodStockDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    // 1: 没有加锁的代码,不会请求延时而阻塞
    //2: 如果加了synchronized那么就上锁,

    // 查询商品的查询对应商品的库存
    public int getStock(String goodId){
        try{
            String sql = "select goods_num from tb_goods where goods_code = ?";
            Integer integer = jdbcTemplate.queryForObject(sql,Integer.class,goodId);
            return  integer;
        }catch(Exception ex){
            ex.printStackTrace();
           return 0;
        }
    }


    // 扣减商品的库存数
    @Transactional(rollbackFor = Exception.class)
    public boolean buy(String userId,String goodId,int stock){
        //商品数量减去1
        String sql = "update tb_goods set goods_num = goods_num - "+stock+" where goods_code = ?";
        //为什么不要使用innobdb的乐观锁,原因很简单:分布式项目中,并发太大可以造成mysql写入的压力太大。
        //String sql = "update tb_goods set goods_num = goods_num - "+stock+" where goods_code = ? and  goods_num -"+stock+">=0";
        int count = jdbcTemplate.update(sql,goodId);
        if(count!=1){
            throw  new BuyException("商品扣减失败...");
        }
        // 添加记录
        String insertSQL = "insert into tb_records (goods_code,user_id,stock) values (?,?,?)";
        int count2 = jdbcTemplate.update(insertSQL,goodId,userId,stock);
        if(count2!=1){
            throw  new BuyException("商品扣减失败...");
        }
        return true;
    }

}
定义redis.properties和解析类

redis.properties 如下:

redis.ip=127.0.0.1
redis.port=6379
redis.password=
redis.max.total=20
redis.max.idle=10
redis.min.idle=2
redis.test.borrow=true
redis.test.return=false

解析 redis.properties 的工具类 PropertiesUtil.java 如下:

package com.zheng.zhengdistributelock.core;

import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

/**
 * Created by mofeng
 */
public class PropertiesUtil {


    private static Properties props;

    static {
        String fileName = "redis.properties";
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
        }
    }

    public static String getProperty(String key){
        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            return null;
        }
        return value.trim();
    }

    public static String getProperty(String key,String defaultValue){

        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            value = defaultValue;
        }
        return value.trim();
    }

}
定义redis的工具类,连接redis
package com.zheng.zhengdistributelock.core;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Created by geely
 */
public class RedisPool {
    private static JedisPool pool;//jedis连接池
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total","20")); //最大连接数
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","20"));//在jedispool中最大的idle状态(空闲的)的jedis实例的个数
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","20"));//在jedispool中最小的idle状态(空闲的)的jedis实例的个数

    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的。
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return","true"));//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。

    private static String redisIp = PropertiesUtil.getProperty("redis.ip");
    private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"));
    private static String password = PropertiesUtil.getProperty("redis.password");


    private static void initPool(){
        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);

        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);

        config.setBlockWhenExhausted(true);//连接耗尽的时候,是否阻塞,false会抛出异常,true阻塞直到超时。默认为true。

        pool = new JedisPool(config,redisIp,redisPort,1000*2,password);
    }

    static{
        initPool();
    }

    public static Jedis getJedis(){
        return pool.getResource();
    }


    public static void returnBrokenResource(Jedis jedis){
        jedis.close();
    }



    public static void returnResource(Jedis jedis){
        jedis.close();
    }


    public static void main(String[] args) {
        Jedis jedis = pool.getResource();
        jedis.set("username","zhangsan");
        returnResource(jedis);
        pool.destroy();//临时调用,销毁连接池中的所有连接
        System.out.println("program is end");
    }



}
分布式锁实现
package com.zheng.zhengdistributelock.redis;

import com.zheng.zhengdistributelock.core.RedisPool;
import redis.clients.jedis.Jedis;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class RedisDistributeLock implements Lock {

    
    private final Long RELEASE_SUCCESS = 1L;
    //获取锁时,睡眠等待5毫秒中
    private long SLEEP_PER = 5;
    private final String key = "lock.key"; 
    private final String value = "lock.value"; 
    private final int expireTime = 5 * 1000; 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    //setnx 版本
    private boolean tryGetDistributeLock2(Jedis jedis,String key ,String value) {
        String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    //lua版本
    private boolean tryGetDistributeLock(Jedis jedis,String key ,String value) {
        try {
            String luescript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then  return redis.call('expire',KEYS[1],ARGV[2])  else return 0 end";
            List<String> keys = new ArrayList<>();
            keys.add(key);
            List<String> argv = new ArrayList<>();
            argv.add(value);
            argv.add("5");
            Object result = jedis.eval(luescript,keys,argv);
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    
    public boolean tryLock() {
        try(Jedis jedis = RedisPool.getJedis();){
            return tryGetDistributeLock(jedis, key, value);
        }
    }
    
    public void lock() {
        try(Jedis jedis = RedisPool.getJedis();){
             while(!tryGetDistributeLock(jedis, key, value)) {
                try {
                    Thread.sleep(SLEEP_PER);
                } catch (Exception e) {
                }
             }
        }
    }
    
    private boolean unLock(Jedis jedis,String key ,String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
    
    public void unlock() {
        try(Jedis jedis = RedisPool.getJedis();){
            unLock(jedis, key, value);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
使用分布式锁解决超卖问题
package com.zheng.zhengdistributelock.service;

import com.zheng.zhengdistributelock.dao.GoodStockDao;
import com.zheng.zhengdistributelock.redis.RedisDistributeLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.Lock;

@Service
@Scope("prototype")
public class GoodsRediskLockSerivice2 {

    @Autowired
    private GoodStockDao goodStockDao;

    Lock lock = new RedisDistributeLock();

    public  boolean buy(String userId,String goodId,int buyNum){
        try {
            if (lock.tryLock()) {
                boolean result = false;
                int num = goodStockDao.getStock(goodId);
                if(num < buyNum){
                    System.out.println("商品库存不足,不在扣减....");
                    return false;
                }
                System.out.println("用户"+userId+",扣减商品:"+goodId+",数量:" + buyNum);
                result =goodStockDao.buy(userId,goodId,buyNum);
                System.out.println("用户"+userId+",扣减商品之后:"+goodId+",结果是:" + result);
                //任务执行完毕 关闭锁
                return result;
            }
        }catch (Exception ex){

        }finally {
            if(lock!=null)lock.unlock();
        }

        return false;
    }
}
测试用例
package com.zheng.zhengdistributelock;

import com.zheng.zhengdistributelock.service.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

@RunWith(SpringRunner.class)
@SpringBootTest
public class zhengDistributelockApplicationTests implements ApplicationContextAware {


    private ApplicationContext applicationContext;
    long timed = 0;


    @Before
    public void start(){
        timed = System.currentTimeMillis();
        System.out.println("开始测试....");
    }


    @After
    public void end(){
        System.out.println("结束测试,执行时长是:" + (System.currentTimeMillis() - timed) / 1000 );
    }


    @Test
    public void buy(){
        //模拟请求数量
        int serviceNum =3;//4台tomcat 107 班 100
        int requesetSize = 100;//每台服务多少并发进入到系统
        //倒计数器。用于模拟高并发 juc CountDownLatch 主线分布式锁,线程的阻塞和唤醒jdk5 juc编程提供并发编程类
        CountDownLatch countDownLatch = new CountDownLatch(1);
        //循环创建N个线程
        List<Thread> threads = new ArrayList<>();

        String userId = "100",goodsId = "apple";
        int stock = 2;

        //模拟服务器的数量
        for (int i = 0; i < serviceNum; i++) {
            //GoodsSerivice goodsSerivice = applicationContext.getBean(GoodsSerivice.class);
            //GoodsLockSerivice goodsSerivice = applicationContext.getBean(GoodsLockSerivice.class);
            //GoodsSyncSerivice goodsSerivice = applicationContext.getBean(GoodsSyncSerivice.class);
            //GoodsZkLockSerivice goodsSerivice = applicationContext.getBean(GoodsZkLockSerivice.class);
            GoodsRediskLockSerivice goodsSerivice = applicationContext.getBean(GoodsRediskLockSerivice.class);
            //GoodsRediskLockSerivice2 goodsSerivice = applicationContext.getBean(GoodsRediskLockSerivice2.class);
            //模拟每台服务器发起请求的数量
            for (int i1 = 0; i1 < requesetSize; i1++) {
                Thread thread = new Thread(()->{
                    try {
                        //等待countdownlatch值为0,也就是其他线程就绪后,在运行后续的代码。
                        countDownLatch.await();
                        //执行吃饭的动作
                        goodsSerivice.buy(userId,goodsId,stock);
                    }catch (Exception ex){
                        ex.printStackTrace();
                    }
                });
                //添加线程到集合中
                threads.add(thread);
                //启动线程
                thread.start();
            }
        }

        //并发执行所有请求
        countDownLatch.countDown();

        threads.forEach((e)->{
            try {
                e.join();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        });

    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

小结

问题1:是不是未来所有的业务都需要用锁来开发呢?

  • 并发量很大,而且资源共享(同一个数据库)并且资源少这个时候可以考虑使用分布式锁
    • 并发量大
    • 共享资源,共享的资源比并发量少
    • 业务不单一的业务
      • 抢购
      • 秒杀
      • 抢红包
      • 抽奖
  • 如偶果仅仅是已查询或者insert有必要有锁吗?

答案:不需要

解决方案

  • 数据库的乐观锁和悲观锁
  • 数据库的无符号
  • redis/zk
  • 利用redis的互斥机制使用分布式锁,解决商品的超卖问题
  • 使用redis的计数器实现业务流水号的功能

8. 利用Redis生成业务流水号思路(分布式订单编号)

目标

系统需要生成根据业务类型生成流水号,每天从1开始生成,第二天会清零继续从0开始,

流水号格式为:

bizCode + date + incr 如:TT-201711230000100。

思路:利用Redis Incr 生成序列号,使用日期加业务编码作为组合Key,这样保证第二天生成的序列号又

是从1开始。

由于我们业务量不是很大,这里在生成序列号之前先判断一下当前key是否存在,若不存在,设置此key过期时间为当天晚上23:59:59,

避免生成很多过期的key。

  • 计数器 count = 0
  • 定时器会每隔24就把count清零

整体设计流程思路如下:

package com.zheng.zhengdistributelock.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class CacheService {

    //这里因为有其他的template,所以名字起得不好看
    @Autowired
    RedisTemplate redisTemplate;

    public Long getIncrementNum(String key) {
        // 不存在准备创建 键值对
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());//0
        // 计数器累加 
        Long counter = entityIdCounter.incrementAndGet();
        System.out.println("=========================>"+ counter);
        if ((null == counter || counter.longValue() == 1)) {// 初始设置过期时间
            System.out.println("设置过期时间为1天!");
            // 设置清除的目的,是让每天的计数器都从0开始
            entityIdCounter.expire(1, TimeUnit.DAYS);// 单位天
        }
        return counter;
    }
}
package com.zheng.zhengdistributelock.redis;

public class SequenceUtils {


    static final int DEFAULT_LENGTH = 3;

    public static String getSequence(long seq) {
        String str = String.valueOf(seq);
        int len = str.length();
        if (len >= DEFAULT_LENGTH) {// 取决于业务规模,应该不会到达3
            return str;
        }
        
        int rest = DEFAULT_LENGTH - len;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < rest; i++) {
            sb.append('0');
        }
        sb.append(str);
        return sb.toString();
    }
}

测试

  @Test
    public void getAutoFlowCodeTest() {
        for (int i = 0; i < 200; i++) {
            String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
            Long num = cacheService.getIncrementNum("demo_get_the_new_" + "test3_"+currentDate);
            String flowCode = SequenceUtils.getSequence(num);
            System.out.println("流水号: " +currentDate+flowCode);

        } 
    }

并发测试

package com.zheng.zhengdistributelock;

import com.zheng.zhengdistributelock.redis.CacheService;
import com.zheng.zhengdistributelock.redis.SequenceUtils;
import com.zheng.zhengdistributelock.service.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.junit4.SpringRunner;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;

@RunWith(SpringRunner.class)
@SpringBootTest
public class zhengDistributelockApplicationTests implements ApplicationContextAware {


    private ApplicationContext applicationContext;
    long timed = 0;


    @Before
    public void start(){
        timed = System.currentTimeMillis();
        System.out.println("开始测试....");
    }


    @Autowired
    private CacheService cacheService;

    @Test
    public void getAutoFlowCodeTest() {
        for (int i = 0; i < 200; i++) {
            String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
            Long num = cacheService.getIncrementNum("demo_get_the_new_" + "test3_"+currentDate);
            String flowCode = SequenceUtils.getSequence(num);
            System.out.println("流水号: " +currentDate+flowCode);

        }
    }



    @After
    public void end(){
        System.out.println("结束测试,执行时长是:" + (System.currentTimeMillis() - timed) / 1000 );
    }


    @Test
    public void buy(){
        //模拟请求数量
        int serviceNum =3;//4台tomcat 107 班 100
        int requesetSize = 100;//每台服务多少并发进入到系统
        //倒计数器。用于模拟高并发 juc CountDownLatch 主线分布式锁,线程的阻塞和唤醒jdk5 juc编程提供并发编程类
        CountDownLatch countDownLatch = new CountDownLatch(1);
        //循环创建N个线程
        List<Thread> threads = new ArrayList<>();

        String userId = "100",goodsId = "apple";
        int stock = 2;

        //模拟服务器的数量
        for (int i = 0; i < serviceNum; i++) {
            //GoodsSerivice goodsSerivice = applicationContext.getBean(GoodsSerivice.class);
            //GoodsLockSerivice goodsSerivice = applicationContext.getBean(GoodsLockSerivice.class);
            //GoodsSyncSerivice goodsSerivice = applicationContext.getBean(GoodsSyncSerivice.class);
            //GoodsZkLockSerivice goodsSerivice = applicationContext.getBean(GoodsZkLockSerivice.class);
            GoodsRediskLockSerivice goodsSerivice = applicationContext.getBean(GoodsRediskLockSerivice.class);
            //GoodsRediskLockSerivice2 goodsSerivice = applicationContext.getBean(GoodsRediskLockSerivice2.class);
            //模拟每台服务器发起请求的数量
            for (int i1 = 0; i1 < requesetSize; i1++) {
                Thread thread = new Thread(()->{
                    try {
                        //等待countdownlatch值为0,也就是其他线程就绪后,在运行后续的代码。
                        countDownLatch.await();
                        //执行吃饭的动作
                        goodsSerivice.buy(userId,goodsId,stock);
                    }catch (Exception ex){
                        ex.printStackTrace();
                    }
                });
                //添加线程到集合中
                threads.add(thread);
                //启动线程
                thread.start();
            }
        }

        //并发执行所有请求
        countDownLatch.countDown();

        threads.forEach((e)->{
            try {
                e.join();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        });

    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

9. Redis缓存击穿的问题?

  • 信号量
  • 限流

抢购,抽奖 - 锁+限流

抢票:CorrentHashMap(锁) + 限流

缓存击穿----信号量解决 + guava(令牌桶)1 500 1 100

10. 基于Redis实现分布式红锁

目标

了解和掌握红锁,已经分析普通的分布式锁存在的问题

分析

因为基于setnx的分布式存在一种集群缺陷。

1、客户端A从master拿到了锁lock

2、master正要把lock同步到(redis的主从是一种异步处理机制)给slave的时候,

突然master因为故障宕机了,导致lock没有同步给slave节点。

3、触发redis的主从切换机制,slave被晋升为master节点。

4、客户端B到master拿lock锁,依然可以拿到。

这样的问题就是:同一把锁被多人同时获取,这样在高并发环境下就会引发很大的故障问题。

以上这种情况下:其实就是CAP定理,即保证了数据一致性,不论redis部署是单机、主从、哨兵还是集群都存在这种风险。如何解决呢?

redis之父antirez提出了红锁的算法,就是为了去解决这个问题。

红锁算法的设计原理

Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁;

**使用场景:**多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);

RedLock为了解决CAP的CP。数据的一致性,采用了n个redis节点,n为奇数,

上图我们有3个master,这3个master完全的独立,不是主从复制或集群。(这里的原理和zookeeper类似)。

为什么集群中N必须是奇数

主要是考虑承载和容错性:

容错性:即在集群环境中,失败实例多少个我们还是可以容忍接受,即系统的CP还是数据的一致。

比如::

在集群环境下,redis失败一台,我们还是可以容忍接受。2n+1 = 2 + 1=3,故部署奇数为3台redis实例,所以在3台的集群中,死掉1

台,剩下2台集群正常工作。

在集群环境下,redis失败2台,我们还是可以容忍接受。2n+1 = 4 + 1=5,故部署奇数为3台redis实例,所以在5台的集群中,死掉2台,

剩下3台集群正常工作。

那为什么是奇数呢,不是偶数?

因为有一个原则:使用资源最少,产生最大的容错。

比如:

在集群环境中,redis失败了1台,我们还是可以容忍接受:奇数2n+1 = 3,偶数:2n+2=4

在集群环境中,redis失败了2台,我们还是可以容忍接受:奇数2n+1 = 5,偶数:2n+2=6

通过以上容忍度不一样,但是部署的实例偶数比奇数多了一台。

番外

需要掌握CAP定理

zookeeper的集群同步原理

部署Redis的3台机器部署

基于配置文件方式
步骤1:把配置文件复制三份

把配置文件redis.conf复制三份,分别是:redis-6380.conf,redis-6381.conf,redis-6382.conf。

把配置文件中对应的端口配置进行修改

port: 6380
port: 6381
port: 6382
步骤2:然后启动对应配置
# 服务端启动
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-server ./conf/redis-6380.conf
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-server ./conf/redis-6381.conf
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-server ./conf/redis-6382.conf
# 客户端启动
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-cli -p 6380
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-cli -p 6381
[root@iZwz94p9y07ns86pck1l2jZ redis-5.0.12]# src/redis-cli -p 6382
步骤3:如果是阿里云服务器或者自己本机安装虚拟机

阿里云服务器:在安全组打开6380:6382的端口

本地虚拟机:关闭防火墙或者打开配置6380:6382的端口

基于docker的方式进行安装
步骤1:安装三台redis的容器服务
docker run -p 6380:6379 --name redis-master-1 -d redis:5.0.7
docker run -p 6381:6379 --name redis-master-2 -d redis:5.0.7
docker run -p 6382:6379 --name redis-master-3 -d redis:5.0.7
基于三台的redis的分布式红锁
步骤1:配置三台服务配置
ksd:
  redis:
    # redis的开关配置,封装在KsdCacheRedisConfiguration 设置模式值有:sentinel/cluster/single
    mode: single
    # 封装在KsdRedisProperties
    database: 0
    password: mkxiaoer1986.
    timeout: 3000
    # 线程池配置,封装在KsdRedisPoolProperties
    pool:
      max-idle: 16
      min-idle: 8
      max-active: 8
      max-wait: 3000
      conn-timeout: 3000
      so-timeout: 3000
      size: 10
    # 单机模式,封装在:KsdRedisSingleProperties
    single:
      address: 47.115.94.78:6379
      address1: 47.115.94.78:6380
      address2: 47.115.94.78:6381
      address3: 47.115.94.78:6382
步骤2:初始化三个redisson服务对象
package com.zheng.travel.config.redis;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "ksd.redis", ignoreInvalidFields = false)
@Data
@ToString
public class KsdRedisProperties {
    private int database;
    /**
     * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
     */
    private int timeout;
    private String password;
    private String mode;
    /**
     * 池配置
     */
    private KsdRedisPoolProperties pool;
    /**
     * 单机信息配置
     */
    private KsdRedisSingleProperties single;
    /**
     * 集群 信息配置
     */
    private KsdRedisClusterProperties cluster;
    /**
     * 哨兵配置
     */
    private KsdRedisSentinelProperties sentinel;
}
package com.zheng.travel.config.redis;
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;

@Data
@ToString
public class KsdRedisClusterProperties {
    /**
     * 集群状态扫描间隔时间,单位是毫秒
     */
    private int scanInterval;
    /**
     * 集群节点
     */
    private String nodes;
    /**
     * 默认值: SLAVE(只在从服务节点里读取)设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。
     * MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取
     */
    private String readMode;
    /**
     * (从节点连接池大小) 默认值:64
     */
    private int slaveConnectionPoolSize;
    /**
     * 主节点连接池大小)默认值:64
     */
    private int masterConnectionPoolSize;
    /**
     * (命令失败重试次数) 默认值:3
     */
    private int retryAttempts;
    /**
     *命令重试发送时间间隔,单位:毫秒 默认值:1500
     */
    private int retryInterval;
    /**
     * 执行失败最大次数默认值:3
     */
    private int failedAttempts;
}
package com.zheng.travel.config.redis;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class KsdRedisSentinelProperties {
    /**
     * 哨兵master 名称
     */
    private String master;
    /**
     * 哨兵节点
     */
    private String nodes;
    /**
     * 哨兵配置
     */
    private boolean masterOnlyWrite;
    /**
     *
     */
    private int failMax;
}
package com.zheng.travel.config.redis;
import lombok.Data;
import lombok.ToString;

@ToString
@Data
public class KsdRedisSingleProperties {
    private String address;
}
package com.zheng.travel.config.redis;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class KsdRedisPoolProperties {
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout;
    private int soTimeout;
    private int size;
}

package com.zheng.travel.config.redis.config;
import com.zheng.travel.config.redis.KsdRedisProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Configuration
@Slf4j
@EnableConfigurationProperties(KsdRedisProperties.class)
public class KsdRedisSingleConfiguration {
    @Autowired
    private KsdRedisProperties redisProperties;
    /**
     * master1
     */
    @Bean
    RedissonClient redissonClient1() {
        log.info("master1-------->redisson single start begin!,{}", redisProperties.getSingle().toString());
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
    /**
     * master2
     */
    @Bean
    RedissonClient redissonClient2() {
        log.info("master2-------->redisson single start begin!,{}", redisProperties.getSingle().toString());
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
    /**
     * master3
     */
    @Bean
    RedissonClient redissonClient3() {
        log.info("master3-------->redisson single start begin!,{}", redisProperties.getSingle().toString());
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
}
步骤3:进行分分布式红锁的控制
package com.zheng.travel.controller.redisson;
import lombok.extern.slf4j.Slf4j;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class RedLockRedissonController {
    @Autowired
    private RedissonClient redissonClient1;
    @Autowired
    private RedissonClient redissonClient2;
    @Autowired
    private RedissonClient redissonClient3;
    @GetMapping("/redlock")
    public void get(String key) throws InterruptedException {
        // 准备三个锁,mylock是就是分布式锁的key sent ma
        RLock rLock1 = redissonClient1.getLock(key);//master 12.15.158.152 key:1
        RLock rLock2 = redissonClient2.getLock(key);//master 12.15.158.154 key:1
        RLock rLock3 = redissonClient3.getLock(key);//master 12.15.158.153 key:1
        //1:创建一个红锁
        RedissonRedLock redissonRedLock = new RedissonRedLock(rLock1, rLock2, rLock3);
            
        boolean islock;
        try {
            islock = redissonRedLock.tryLock(1000 * 5, 1000 * 60 * 5, TimeUnit.MILLISECONDS);
            log.info("线程:{},是否拿到锁:{}", Thread.currentThread().getName(), islock);
            if (islock) {
                log.info("线程:{},获取锁成功!", Thread.currentThread().getName());
                //模拟业务执行超时30分钟
                Thread.sleep(1000 * 60 * 30);
            }
        } catch (Exception ex) {
            log.error("get lock is error");
        } finally {
            // 无论如何都要释放锁
            log.info("线程:{},释放锁成功!", Thread.currentThread().getName());
            redissonRedLock.unlock();
        }
    }
}

十五、知识小结

1:jdk sychronized,lock是属于jdk本自身,在单体可以使用,在集群和分布式微服务开发者(ribbon),可能

共享资源泄露问题 (仅仅了解,如果在单体架构可以考虑)

2:基于数据库乐观锁和悲观锁,都属于数据级别的锁,虽然能解决问题,但是在多线程高并发的情况,它的吞吐

量不高。而且很容易给数据造成很大压力。很容易宕机的情况。(不会使用或者在并发量非常少情况下可以考

虑)

3:使用Redis的分布式锁,它是一种乐观锁的机制,在多线程高并发的情况,某个线程获取到锁,另外的线程就

会全部失败。也就是说适合读多写少的情况,比如:用户注册,账户余额体现的情况。但是不适合用于下单或者

抢购。

4:zk的分布式锁,利用一直顺序临时节点 + watcher监听机制(一句话:排队取号机制)来完成的分布式锁。这

种锁的具有可重入性,如果一旦多线程并发进来的,如果有一个线程获取到锁,那么其他的线程就阻塞等待,它

一定会获取到锁,只不过获取锁它由时间长度,

如果超过这个时间长度就或释放当前锁,让其他阻塞的线程去获取锁。A —yykk-lock 20s B—xiaoyu-lock 20s

—下单 抢购中。(推荐)

5:Resssion弥补:redis的可重入性和高并发的问题。(推荐)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodingW丨编程之路

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值