基于Redis实现分布式锁
1. 分布式锁介绍
什么是锁?在多线程(多进程)应用程序中,当需要访问到共同的资源时,尤其涉及到写操作的时候,如果不对资源访问做同步处理,会发生无法预料的情况。锁就是在程序中对资源访问做同步处理的,把异步变同步。java,数据库等都有锁的概念。
那么什么是分布式锁呢?程序开发直至今日,许多的项目,尤其是互联网项目,单个服务已经无法满足需求了。许多的项目都是同时部署多个机器节点。这个时候如果要对同个资源做同步控制时,传统的锁已经无法解决问题了。比如java,我们知道java应用中的锁只能局限于单个jvm才能生效,但是多个jvm的情况下,锁就失去了作用。这个时候就要用到分布式锁了,保证某一时刻只能有一个客户端获取到锁。一般情况下,分布式锁在应用程序层面已经实现不了,这个时候需要借助到三方中间应用来实现分布式锁。比如,数据库,redis,zookeeper。本文就是讲如何使用redis实现分布式锁。
首先谈谈个人对分布式的注意点:
(1)互斥性:与单个java应用一样,只能有一个线程获取到锁。在分布式锁中,也只能在某一时刻只有一个客户端获取到锁。
(2)重入性:对于java中无论是synchronized还是Lock锁,线程获取到锁后都是可以支持再次获取锁的,那么对于分布式锁来说,理应支持某个客户端获取到了锁后,依然可以再次获取到锁。
(3)谁获得锁谁解锁:在java应用对于锁来说都是默认的支持当前线程获取到的锁只能有当前线程解锁,但是在分布式锁中,如果客户端在解锁时不做校验,很容易发生其它的客户端解锁的现象,这样很容易导致锁失去意义。
(4)死锁:在java应用中,死锁多发生与多个线程循环获取到了多个锁,但同时需要获取其它锁从而导致循环等待从而发生死锁的现象。但是对于分布式锁来说,即使获取同一个锁,也容易发生死锁的现象,比如用数据库实现分布式锁,某个获取到锁的客户端突然宕机了,锁无法释放。redis获取锁不做过期时间设置,zookeeper用永久节点来获取锁等情况都容易导致死锁。
2. Redis实现分布式锁原理
实现分布式锁最重要的是确保多个客户端同时获取锁只能有一个客户端能获取到锁,其他的客户端都获取不到锁。redis提供了许多很好用的命令来确保当并发情况下操作时只有一个连接能操作成功,比如setnx命令,下面就以这个命令来实现分布式锁。
nx的含义:当redis中不存在key就设置成功,如果存在key就设置失败,那么当多个连接同时使用nx命令来设置key时只有一个连接会操作成功,这个时候我们就可以认为该连接获取到了锁,其它连接获取锁失败。当获取到锁的连接用完后释放锁就把对应的key删除掉,这样其它的连接就可以去竞争获取锁了。为了防止获取到锁的连接没能正常关闭,导致其它连接无法获取到锁从而出现死锁的情况,我们可以给key设置一个过期时间,以防止死锁。下面是代码。
redis jar包依赖
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
代码实现
public class RedisLock { private static JedisPool jedisPool; //uuid,用于区分不同客户端 private String uid; //锁的名称(key) private String lockName; //锁过期时间(防止死锁) private Integer expireTime; static { JedisPoolConfig config = new JedisPoolConfig(); // 设置最大连接数 config.setMaxTotal(200); // 设置最大空闲数 config.setMaxIdle(8); // 设置最大等待时间 config.setMaxWaitMillis(1000 * 100); // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的 config.setTestOnBorrow(true); jedisPool = new JedisPool(config, "127.0.0.1", 6379, 3000, "123456"); } public RedisLock(String lockName, Integer expireTime) { this.lockName = lockName; this.expireTime = (expireTime == null || expireTime <= 0) ? 100 : expireTime; this.uid = UUID.randomUUID().toString(); } /** * @描述: 获取锁 * @作者: * @时间: */ public Boolean getLock() { Jedis jedisClient = jedisPool.getResource(); try { //用uid加上线程id表示value,用于锁释放判断是否是当前线程获取到的锁 String lockValue = uid + Thread.currentThread().getId(); //用nx命令设置key并加上过期时间 String result = jedisClient.set(lockName, lockValue, "nx", "ex", expireTime); return "ok".equalsIgnoreCase(result); } catch (Exception e) { //操作异常 e.printStackTrace(); return false; } finally { if (jedisClient != null) { jedisClient.close(); } } } /** * @描述: 释放锁 * @作者: * @时间: */ public void releaseLock() { String lockValue = uid + Thread.currentThread().getId(); Jedis jedisClient = jedisPool.getResource(); try { String result = jedisClient.get(lockName); if (lockValue.equalsIgnoreCase(result)) { //是当前线程获取到的锁,释放 jedisClient.del(lockName); } } catch (Exception e) { } finally { if (jedisClient != null) { jedisClient.close(); } } } }
总结:以上虽然实现了锁的基本要求,但是可以看出来有以下几个缺点:
(1)没有实现可重入锁的概念,当前线程无法多次获取锁,这种实现可以通过改造key的value来实现重入锁;
(2)过期时间不好控制,如果在过期时间内当前获取到锁的线程还没执行完业务,锁就自动释放了,可能会导致其它线程获取到锁了。这个就得需要在获取到锁后加上监听事件了,当锁即将自动过期时更新过期时间,Redisson框架实现分布式锁就用到了这种监听;
(3)锁的释放不是原子性操作,这个解决方法可以通过lua脚本来解决;
总之redis的这种实现分布式锁还是存在一定的问题,在高并发的情况下很容易出现一直获取不到锁的情况,无法实现有序获取锁,这应该是用redis实现分布式锁最大的一个弊端。
注意:本文仅代表个人理解和看法哟!和本人所在公司和团体无任何关系!