分布式锁

锁类型

1.排他锁

​ 简称X锁,又称为独占锁或写锁。一般针对操作被加上锁的资源,在使用的时候只允许对应的事务使用。不允许别的事务使用,直到对应事务释放这个锁,别的事务才能访问这个资源

​ zookeeper就是使用这个解决分布式锁

2.共享锁

​ 简称S锁,一般是针对读取操作。加上共享锁之后,数据对所有事物都可见。也就是别的事务或者线程都可以使用这个锁

zookeeper锁执行机制

1.定义锁(创建锁)

java中定义锁有两种方式:

  1. synchronized
  2. JDK5的ReentrantLock,重入锁(自己抢到的锁,自己还可以再抢到这个锁,别人抢不到

2.获取锁

ookeeper中创建临时节点。

  1. 如果是读请求,就创建共享锁。也就是多个请求都可以使用同一个资源
  2. 如果是写请求,就创建排他锁,这样就可以保证一个数据的修改只会被一个事务在同一时间操作

3. 释放锁

  1. 获取锁的客户端岱机,会自动断开连接,就会删除这个临时节点,也就释放锁了
  2. 正常业务逻辑执行完毕,可以手动删除这个临时节点

Redis分布式锁

**分布式锁:**控制分布式系统有序的去对资源进行操作,通过互斥的方式保持一致.

例子:假入共享资源是ATM机,分布式系统就是要取钱的人,分布式锁 就是保证这个ATM机一次只有一个人可以使用,一个人使用的时候就会将这个装ATM机的屋子锁上,第二个人想要进去就需要有这个钥匙来打开这个门。由于没有钥匙,所以就得等到第一个人出来,然后第二个人拿着钥匙进去。以此类推.

解决分布式锁的核心思路

在多台服务器集群上,只能保证一个JVM进行操作

分布式锁解决方案

  1. 基于数据库
  2. 基于redis实现分布式锁(setnx方法)。setnx可以存入值,如果存入值成功就返回1。如果存入的key已经存在了,就返回0。所以多个jvm同时set一个键,只要成功就可以返回1.就可以继续执行
  3. 基于Zookeeper实现,Zookeeper:是一个分布式协调工具。在分布式解决方案中可以实现消息中间件注册中心分布式锁(多个jvm,在zk上创建相同的临时节点,因为临时节点的路劲是保证唯一的,只要谁能创建节点成功,就会获取锁。没有创建成功节点,就会进行等待,当释放锁的时候,采用事件通知jvm重新获取锁的资源)

初步理解redis如何实现分布式锁

多个客户端(JVM),使用setnx命令,在redis上创建相同的key,因为redis的key不允许重复,只要谁创建key成功,就能获得这个锁,没有创建成功,就会进行等待

核心思路:保证只有一个JVM进行操作

重点分析redis实现分布式锁

关于redis的setnx命令

setnx命令的作用:也可以做写入key操作,可以获取返回结果。如果返回结果时1,就表示key不存在,写入成功。如果返回结果是0,表示key存在,写入失败不覆盖

怎么释放锁?

这也是和zk实现分布式锁最大的区别

在执行完操作的时候,删除key,如果删除失败,就会导致思索,所以还需要给这个key设置有效时间,有效时间就是防止死锁.

如果直接删除key,可能会出现这种情况,a获取的锁被b删除了,所以需要保证是自己创建的和删除自己的.

代码实现

代码实现的重点是:

​ 1.在释放锁的时候,需要判断是否是对应的线程释放自己的锁,也就是不能出现a创建锁,结果被b释放了

1.redis在IDEA中的配置

依赖

  <dependencies>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.2</version>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

    </dependencies>

yml配置文件

server:
  port: 8084
spring:
  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp
  datasource:
    username: root
    url: jdbc:mysql://localhost:3306/xxx?characterEncoding=utf8&serverTimezone=UTC
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    #    redis.cluster集群的节点
    cluster:
      nodes: 39.97.252.228:8001,39.97.252.228:8002,39.97.252.228:8003,39.97.252.228:8004,39.97.252.228:8005,39.97.252.228:8006,39.97.252.228:8007,39.97.252.228:8008
    jedis:
      pool:
        #      连接池最大数量
        max-active: 10
        #        连接池最小空闲连接
        min-idle: 1
        max-idle: 2
        #        连接池最大阻塞时间
        max-wait: -1
    password: 123456
    host: 39.97.252.228
    timeout: 1000
    commandTimeout: 5000

因为redis集群加密了,所以的有配置类配置一下

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.util.HashSet;
import java.util.Set;

@Configuration
@ConditionalOnClass({JedisCluster.class})
public class RedisConfig {
    @Value("${spring.redis.cluster.nodes}")
    private String clusterNodes;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.commandTimeout}")
    private int commandTimeout;
    @Bean
    public JedisCluster getJedisCluster() {
        System.out.println(clusterNodes+"******************");
        String[] cNodes = clusterNodes.split(",");
        Set<HostAndPort> nodes = new HashSet<>();
        //分割出集群节点
        for (String node : cNodes) {
            String[] hp = node.split(":");
            nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
        }
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        //创建集群对象。没有密码的请使用这一个
        // JedisCluster jedisCluster = new JedisCluster(nodes,commandTimeout);
        //有密码的请使用这一个。 我这里是redis有密码的所以我使用的这一个
        return new JedisCluster(nodes,commandTimeout,commandTimeout,5,password, jedisPoolConfig);
    }
}

创建锁与释放锁

package com.soa.lock.coderymy;

import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisCluster;

import java.util.UUID;

public class LockRedis {

    @Autowired
    private JedisCluster jedisCluster;
    /**
     * 基于redis实现的分布式锁的代码思路
     * 获得锁
     * 释放锁
     */
    //定义key的名字
    private String redisLockKey = "redis_lock";
    /**
     * 获得锁
     * @param acquireTimeout
     * @param timeout
     * @return
     */
    public String getRedisLock(String lockKey, Long acquireTimeout,Long timeout){
        /*
         * 1. 建立redis连接好几种都行
         * 2. 定义redis的key对应的value的值,可以使用uuid生辰(不唯一)。在释放锁的时候使用
         * 3. redis实现分布式锁,定义两个超时的时间问题:
         *   1. 获取锁之前:获取锁的超时时间,在尝试获取锁的时候,如果在规定的时候还没有获取到就放弃
         *   2. 获取锁成功之后:key对应有有效期
         * 4. 使用循环机制,保证重复进行尝试获取锁(乐观锁的原理)
         * 5. 使用setnx命令插入对应的redisLockKey,如果返回为1就获取锁成功
         * */
        String retIdentifierValue = null;
        try {
            //创建value
            String identifierValue = UUID.randomUUID().toString();
            //创建key
            String lockName = redisLockKey+lockKey;
            //创建key_value的存活时间
            int expriseLock = (int)(timeout/1000);
            //创建获取锁的循环等待时间
            Long endTime = System.currentTimeMillis()+acquireTimeout;
            //进入循环
            while ((System.currentTimeMillis() < endTime)){
              //使用setnx判断key是否存在("redis_lock",identifierValue )
                if(jedisCluster.setnx(lockName,identifierValue)==1){
                    jedisCluster.expire (lockName,expriseLock);
                    retIdentifierValue = identifierValue;
                    return retIdentifierValue;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retIdentifierValue;
    }

    /**
     * 解锁
     * 必须验证身份再解锁
     * @param lockKey
     * @param identifier
     * @return
     */
    public boolean unRedisLock(String lockKey,String identifier){
        boolean flag = false;
        try {
            //定义锁的名字
            String lockName = "redis_lock"+lockKey;
            // 3.如果value与redis中一直直接删除,否则等待超时
            if (identifier.equals(jedisCluster.get(lockName))) {
                jedisCluster.del(lockName);
                System.out.println(identifier + "解锁成功......");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return flag;
    }
}

使用

import com.soa.lock.coderymy.LockRedis;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
public class LockService {

    LockRedis lockRedis = new LockRedis();

    public void seckill() {

        String identifier = lockRedis.getRedisLock("itmayiedu", 5000l, 5000l);
        if (StringUtils.isEmpty(identifier)) {
            // 获取锁失败
            System.out.println(Thread.currentThread().getName() + ",获取锁失败,原因时间超时!!!");
            return;
        }
        System.out.println(Thread.currentThread().getName() + "获取锁成功,锁id identifier:" + identifier + ",执行业务逻辑");
        try {
//            Thread.sleep(30);
        } catch (Exception e) {

        }
        // 释放锁
        boolean releaseLock = lockRedis.unRedisLock("itmayiedu", identifier);
        if (releaseLock) {
            System.out.println(Thread.currentThread().getName() + "释放锁成功,锁id identifier:" + identifier);
        }
    }
}

测试

   @Test
    public void test2(){
        LockService lockService = new LockService();
        for (int i = 0; i < 50; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lockService.seckill();
                }
            }).start();
        }
    }

问题的总结

1.为什么要设置redis的key值的过期时间

​ 为了解决死锁问题,也就是有一种可能就是释放锁失败,这样的话,就会造成接下来的操作都无法获取锁

2.一般这个超时时间可以自己判断进行设置,一般都是要求对应的业务执行完毕的时间紧

zookeeper分布式锁

1.zookeeper中的watch监听器

2.临时顺序节点

3.写操作排他锁,读操作共享锁

分布式锁的使用场景

案例:需要生成订单号。使用UUID、时间错+业务id(唯一,不允许重复)
幂等:不重复,唯一

生成订单号

public class OrderNumGenerate{
    private int count;
    
    public String getNumber(){
        SimpleDataFoemat simpt=new SimpleDataFormat("yyyy-MM-dd hh-mm-ss");
        return simpt.format(new Date())+"-"+ ++count;
    }
}

问题

生成订单号,使用多线程的方法会出现线程安全问题。也就是在同一时间生成的时间戳等,可能会导致生成的订单号并不唯一。可能会出现很多的相同的

所以多线程共享同一个全局变量,可能会受到其它线程的干扰,这就是线程安全的问题

zookeeper

什么是Zookeeper

是一个分布式协调工具,可以实现很多功能,比如注册中心,分布式锁,负载均衡命名服务,分布式通知/协调,发布订阅(MQ),集群环境的选举策略(哨兵机制)

zk的存储数据结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9VKEw3R-1576905823867)(D:\总结\分布式事务和锁\img\zookeeper数据结构2.png)]

Zookeeper实现分布式锁

  1. 使用zk创建临时节点
  2. 谁能创建节点成功,谁就可以拿到锁,就能生成订单号。
  3. 释放锁,临时节点。(和会话连接保持一样的状态,只要会话结束,就会删除这个临时节点)

总结: 在zk上创建临时节点,只要谁能创建节点成功,其它没有创建成功就等待。其它服务器使用事件监听,获取节点通知。只要节点被删除,就应该去获取锁资源

代码实现

1.导入依赖

   <dependency>

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

               <artifactId><u>zkclient</u></artifactId>

               <version>0.10</version>

          </dependency>

2.创建锁的接口

public interface Lock {

    //获取到锁的资源

     public void getLock();

    // 释放锁

     public void unLock();

}

3.创建实现类

//将重复代码写入子类中..
public abstract class ZookeeperAbstractLock implements Lock {
      // <u>zk</u>连接地址
      private static final String CONNECTSTRING = "127.0.0.1:2181";
      // 创建<u>zk</u>连接
      protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
      protected static final String PATH = "/lock";
      public void getLock() {
            if (tryLock()) {
                  System.out.println("##获取lock锁的资源####");
            } else {
                  // 等待
                  waitLock();
                  // 重新获取锁资源
                  getLock();
                  //也就是等待到信号量通知可以获取锁了,然后继续获取锁。这里并没有导致死循环,因为只要获取到锁,就会输出,从而不会继续执行else的代码
            }
      }
      // 是否获取锁,获取锁成功true,失败false
      abstract boolean tryLock();
      // 等待
      abstract void waitLock();
      public void unLock() {
            if (zkClient != null) {

                  zkClient.close();
                  System.out.println("释放锁资源...");
            }
      }
}

3.创建子类,来实现tryLock()和waitLock()方法

public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
      private CountDownLatch countDownLatch = null;
      @Override
      boolean tryLock() {
            try {
            //这里进行尝试创建锁,也就是使用同一个path创建锁,成功这里就返回true。继续指向上一个类的方法。否则返回false  
                  zkClient.createEphemeral(PATH);
                  return true;
            } catch (Exception e) {
//                e.printStackTrace();
                  return false;
            }

      }
      @Override
      void waitLock() {
                  //创建一个事件监听的方法
            IZkDataListener izkDataListener = new IZkDataListener() {
                        //当监听的节点被删除的时候走这里
                  public void handleDataDeleted(String path) throws Exception {
                       // 监听到节点被删除了,就唤醒被等待的线程
                       if (countDownLatch != null) {
                             countDownLatch.countDown();
                       }
                  }
                  //当监听的方法被修改走这里
                  public void handleDataChange(String path, Object data) throws Exception {
                  }
            };
            // 注册事件
            zkClient.subscribeDataChanges(PATH, izkDataListener);
            if (zkClient.exists(PATH)) {
            //也就是已经有人在用这个锁,就创建信号量
                  countDownLatch = new CountDownLatch(1);
                  try {
                       countDownLatch.await();
                  } catch (Exception e) {
                       e.printStackTrace();
                 }
            }
            // 删除监听
            zkClient.unsubscribeDataChanges(PATH, izkDataListener);
      }
}

4.测试

public class OrderService implements Runnable {
      private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
      // 使用lock锁
      // private java.util.concurrent.locks.Lock lock = new ReentrantLock();
      private Lock lock = new ZookeeperDistrbuteLock();
      public void run() {
            getNumber();
      }
      public void getNumber() {
            try {
                  lock.getLock();
                  String number = orderNumGenerator.getNumber();
                  System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number);
            } catch (Exception e) {
                  e.printStackTrace();
            } finally {
                  lock.unLock();
            }
      }
      public static void main(String[] args) {
            System.out.println("####生成唯一订单号###");
//          OrderService orderService = new OrderService();
            for (int i = 0; i < 100; i++) {
                  new Thread( new OrderService()).start();
            }
      }
}

总结

为什么需要使用分布式锁

使用最多的一个例子就是,在并发的情况下,创建业务id。使用时间戳或者怎么样,可能会创建重复的业务id。所以需要使用分布式锁来锁住正在创建的业务id。以防止创建相同的id

什么是Zookeeper

Zookeeper是分布式的一套协调工具,它支持了很多的分布式解决方案。如注册中心,分布式锁,负载均衡命名服务,分布式通知/协调,发布订阅(MQ)等等。都可以得到解决

使用Zookeeper实现分布式锁的原理

创建锁

使用相同点路径创建节点。由于相同路径的节点只能存在一个。所以别的线程去创建已经存在的节点的时候就会失败。这个时候就让线程进入等待,知道该线程代码执行完毕,才释放这个节点,这个时候使用事件监听 的方法。只要监听到节点的删除,就唤醒别的线程来创建这个节点。这里线程的唤醒与等待,使用了信号量的概念。

释放锁

由于创建的是临时节点,所以只要断开客户端的链接,就会使这个节点被删除

释放锁,临时节点。(和会话连接保持一样的状态,只要会话结束,就会删除这个临时节点)。

redis实现分布式锁与Zookeeper实现分布式锁的区别
相同点

在集群的环境下,保证只允许有一个jvm进行执行

从技术上分析
Redis是nosql数据库,主要特点是缓存
Zookeeper是分布式协调工具,主要用户分布式解决方案

实现思路分析
获取锁

Zookeeper,多个客户端(jvm)会在Zookeeper上创建同一个临时节点,因为Zookeeper节点命名路径保证唯一,不允许出现重复,只要谁能创建成功就能获取这个锁
redis,多个jvm会在redis中使用setnx命令创建相同的一个key,因为redis的key保证唯一,不允许重复。只要谁先创建成功,谁就能获取锁

释放锁

Zookeeper直接关闭 临时节点session会话连接,因为临时节点生命周期session会话绑定在一起,如果session会话连接关闭的话,就会使这个临时节点被删除
然后客户端使用事件监听,监听到临时节点被删除,就释放锁

redis释放锁的时候,为了保证是锁的一致性问题,在删除锁的时候需要判断对应的value是否是对应创建的那个业务的id

死锁解决
Zookeeper使用会话有效方式解决死锁的现象
redis是对key设置有效期来解决死锁现象

性能上:
因为redis是nosql,所以redis比Zookeeper性能好
可靠性
Zookeeper更加可靠。因为redis有效求不是很好控制,可能会产生延迟

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值