Redis与ZooKeeper实现分布式锁

  • 分布式锁
  • Jedis实现分布式锁
  • 添加依赖
  • 编码实现
  • RedisTemplate实现分布式锁
  • 注意事项
  • 编码实现
  • Redisson实现分布式锁
  • 添加依赖
  • 配置Redisson
  • 编码实现
  • ZooKeeper实现分布式锁
  • znode
  • 添加依赖
  • 配置Zookeeper
  • 编码实现


分布式锁

对于单机多线程来说,在Java中,通常使用 ReetrantLock 类、synchronized 关键字这类JDK自带的本地锁来控制一个 JVM 进程内的多个线程对本地共享资源的访问。

分布式系统下,不同的服务运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁就诞生了。

分布式锁是一种可以在分布式系统中协调不同节点之间访问共享资源的锁。它允许多个进程或线程尝试获取锁,但只有一个进程或线程能够成功获取锁并执行操作,其他的进程或线程则需要等待锁释放后再次尝试获取。

分布式锁的实现通常使用Redis 或者 ZooKeeper。

一个最基本的分布式锁需要满足:

互斥 :任意一个时刻,锁只能被一个线程持有

高可用 :锁服务是高可用的。即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问

可重入:一个节点获取了锁之后,还可以再次获取锁

Jedis实现分布式锁

添加依赖

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

编码实现

public class RedisLockTest {

    private static int count = 0;
    private static String lockKey = "lock";

    private static void call(Jedis jedis) {
        // 生成标识,由于超时原因可能导致释放其他线程的锁,释放锁时保证是释放自己的锁
        String requestId = UUID.randomUUID().toString();
        // 加锁
        boolean locked = tryLock(jedis, lockKey, requestId, 60);
        try {
            if (locked) {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            unlock(jedis, lockKey, requestId);
        }
    }

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  锁的值
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // 自旋锁
        while (true) {
            // set key value ex seconds nx(只有键不存在的时候才会设置key)
            String result = jedis.set(lockKey, requestId, SetParams.setParams().ex(expireTime).nx());
            if ("OK".equals(result)) {
                return true;
            }
        }
    }

    /**
     * 释放分布式锁
     *
     * @param jedis   Redis客户端
     * @param lockKey 锁
     * @return 是否释放成功
     */
    public static boolean unlock(Jedis jedis, String lockKey, String requestId) {
        if (!requestId.equals(jedis.get(lockKey))) {
            return false;
        }
        Long result = jedis.del(lockKey);
        return 1L == result;
    }

    public static void main(String[] args) throws Exception {
        RedisLockTest redisLockTest = new RedisLockTest();
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMinIdle(1);
        jedisPoolConfig.setMaxTotal(5);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 1000, null);
        Thread t1 = new Thread(() -> RedisLockTest.call(jedisPool.getResource()));
        Thread t2 = new Thread(() -> RedisLockTest.call(jedisPool.getResource()));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(redisLockTest.count);
    }
}

RedisTemplate实现分布式锁

加锁: 调用set命令设置值,能够设置成功则表示加锁成功  set lock_key lock_value NX PX 5000

释放锁: 调用del命令来删除设置的键值  del lock_key

注意事项

当某个线程获取锁后程序挂掉,此时还没来得及释放锁,这样后面所有的线程都无法获取锁,所以在加锁时设置一个过期时间防止死锁。

加锁和解锁必须是同一个客户端,所以在加锁时可以设置当前线程id,在释放锁时判断是否为当前线程加的锁,如果是再释放锁。

编码实现

Lua脚本使用参考:http://redis.cn/commands/set.html

@Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/test")
    public String test() {
        // 执行业务

        // 执行加锁业务
        this.redisLock();
        
        // 执行业务
        return "success";
    }


    public Boolean redisLock() {
        // 获取当前线程id
        String threadId = Thread.currentThread().getId() + "";

        // 开始加锁
        Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock", threadId, 5000, TimeUnit.MILLISECONDS);
        if (locked) {
            try {
                //加锁成功
                int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
                if (stock > 0) {
                    stock--;
                    redisTemplate.opsForValue().set("stock", stock + "");
                    log.info("库存扣减成功,剩余库存:{}", stock);
                } else {
                    log.info("库存不足,剩余库存:{}", stock);
                }
            } finally {
                // Lua脚本
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //删除锁
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), threadId);
                return true;
            }

            // 最优删除锁方式还是使用lua脚本
            // 防止因执行耗时,缓存已过期,其他线程持有锁时,当前线程才执行完,然后执行释放锁操作,误删其他线程的锁
//                String lockValue = redisTemplate.opsForValue().get("lock");
//                if (threadId.equals(lockValue)) {
//                    //释放锁
//                    redisTemplate.delete("lock");
//                }
        } else {
            log.info("获取锁失败...");
            try {
                Thread.sleep(500);
            } catch (Exception e) {
            }
            // 自旋锁方式
            return redisLock();
        }
    }

Redisson实现分布式锁

Redisson内置提供了基于Redis的分布式锁实现,该方式是推荐的分布式锁使用方式。

官网:https://redisson.org/

GitHub地址:https://github.com/redisson/redisson

添加依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.16.6</version>
</dependency>

配置Redisson

@Configuration
public class RedissonConfig {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        config.useSingleServer().setPassword(password);
        return Redisson.create(config);
    }
}

编码实现

@Autowired
    private RedissonClient redissonClient;
    	
    @GetMapping("/test")
    public String test() {
        //获得分布式锁对象
        RLock lock = redissonClient.getLock("lock");
        try {
            // 加锁以后5秒钟自动解锁,无需调用unlock方法手动解锁
            lock.lock(5000, TimeUnit.MILLISECONDS);
            
            // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
			//  boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock--;
                redisTemplate.opsForValue().set("stock", stock + "");
                log.info("库存扣减成功,剩余库存:{}", stock);
            } else {
                log.info("库存不足,剩余库存:{}", stock);
            }
        } catch (Exception exception) {
            log.info("出现异常....");
        } finally {
            //解锁
            lock.unlock();
        }
        return "success";
    }
}

ZooKeeper实现分布式锁

znode

每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。

Zookeeper中节点分为4种类型:

持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在

持久顺序节点(PERSISTENT_SEQUENTIAL)

顺序节点就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号

临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

临时顺序节点(EPHEMERAL_SEQUENTIAL)

临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

Zookeeper实现分布式锁的原理是基于Zookeeper的临时顺序节点

Apache Curator 是用于分布式协调服务Apache ZooKeeper的 Java/JVM 客户端库。它包括一个高级 API 框架和实用程序,使用 Apache ZooKeeper 变得更加容易和可靠。

Apache Curator官网: https://curator.apache.org/

添加依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.2.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.14</version>
        </dependency>

配置Zookeeper

@Configuration
public class ZkConfig {
    @Bean
    public CuratorFramework curatorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("IP:2181")
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        return client;
    }
}

编码实现

@Autowired
    private CuratorFramework curatorFramework;
    
	@GetMapping("/test")
    public String test() {
        InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/lock");

        try {
            boolean locked = mutex.acquire(0, TimeUnit.SECONDS);
            if (locked) {
                int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
                if (stock > 0) {
                    stock--;
                    redisTemplate.opsForValue().set("stock", stock + "");
                    log.info("库存扣减成功,剩余库存:{}", stock);
                } else {
                    log.info("库存不足,剩余库存:{}", stock);
                }
                //释放锁
                mutex.release();
            } else {
                log.info("获取锁失败...");
            }
        } catch (Exception exception) {
            log.info("出现异常....");
        }

        return "success";
    }