前言
上回说到如何应用 zookeeper 实现分布式锁,它是通过节点的新建和删除来实现的,这种频繁的 io 操作在并发很高的状况下必定是不实用的,那这节咱们来看看如何应用 redis 实现分布式锁。
咱们都晓得 redis 最大的劣势就是速度快,大部分操作都是在内存中间接实现的,这就和 io 操作不在一个数量级上了。
手动实现分布式锁
实现逻辑
首先咱们要理清 redis 实现分布式锁的逻辑:在 redis 中,咱们次要通过 setnx 这个命令来实现分布式锁,这个命令什么意思呢?set not exit,只有在 key 不存在时,才会设值胜利 ,这就能够模拟出各个客户端过去拿锁的过程,当多个客户端同时过去获取锁时,当有一个 setnx 胜利时,那其余客户端都会失败,那这些失败的客户端怎么办呢?咱们能够通过循环期待来反复获取锁,直到获取锁胜利。
获取到锁的客户端能够通过删除节点来开释锁,这里有一点须要留神:在 zookeeper 中能够通过长期节点来避免客户端宕机等起因造成的死锁问题,那在 redis 中就须要无效工夫来避免了,当客户端在肯定工夫范畴内还没有开释锁的话,就须要主动删除节点来开释锁。
留神点
1、在获取锁的时候须要 setnx 和设置无效工夫,这两步必须原子操作,否则在高并发状况下必定会出问题。
从 Redis 2.6.12 版本开始,set 命令的行为能够通过一系列参数来批改:SET key value [EX seconds] [PX milliseconds] [NX|XX]
,通过参数设值,能够同时实现 setnx 和 setex 两种成果,因而不必散布执行,这就保障了操作的原子性。
2、在获取锁设值的时候,须要设值一个线程的惟一 id,这里能够应用 uuid 来实现,在开释锁的时候,也就是删除 key 须要判断值是否和以后线程的 uuid 一样,一样才示意是以后持有的锁。
代码实现
测试 redis 客户端选用 jeds,须要引入 jedis 包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
写一个工具类来获取 jedis
public class RedisUtil {
private static volatile JedisPool jedisPool = null;
private RedisUtil(){}
public static JedisPool getJedisPool() {if (jedisPool == null) {synchronized (RedisUtil.class) {if (jedisPool == null) {GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(20);
jedisPool = new JedisPool(config, "127.0.0.1", 6379);
}
}
}
return jedisPool;
}
public static Jedis getJedis() {return getJedisPool().getResource();}
}
下面 jedis 连接池应用单例来保障唯一性,如果应用 spring 框架的话,能够交给 spring 来治理,我这里只是单纯测试类。
接着写获取锁和开释锁办法
public interface RedisLock {boolean tryLock(String key, String value);
void unLock(String key, String value) throws Exception;
}
public class RedisLockImpl implements RedisLock {
@Override
public boolean tryLock(String key, String value) {
try {Jedis jedis = RedisUtil.getJedis();
// 胜利返回 OK,失败返回 null
String set = jedis.set(key, value, "NX", "PX", 10000);
return set != null;
} catch (Exception e) {e.printStackTrace();
}
return false;
}
@Override
public void unLock(String key, String value) throws Exception{Jedis jedis = RedisUtil.getJedis();
String s = jedis.get(key);
// 判断以后 uuid 是否和 redis 中一样
if (s == null || !s.equals(value)) {throw new Exception("此对象没有获取锁,无奈开释");
}
jedis.del(key);
}
}
最初咱们来测试一下,因为在本地是单机状况下,能够应用多线程来模仿分布式。
public class RedisLockTest {private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 20, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(1024));
public static void main(String[] args) throws Exception{RedisLock redisLock = new RedisLockImpl();
String key = "lock:demo:1";
for (int i = 0; i < 2; i++) {threadPoolExecutor.execute(() -> {
// 线程惟一 id
String value = UUID.randomUUID().toString();
// 循环期待获取锁
while (true) {
try {boolean flag = redisLock.tryLock(key, value);
if (flag) {System.out.println(Thread.currentThread().getName() + "取得锁胜利,开始工作");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "工作一秒实现,开释锁");
redisLock.unLock(key, value);
break;
} else {System.out.println(Thread.currentThread().getName() + "取得锁失败");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "期待 2s");
}
} catch (Exception e) {e.printStackTrace();
}
}
});
}
}
}
后果:pool-1-thread- 1 取得锁胜利,开始工作
pool-1-thread- 2 取得锁失败
pool-1-thread- 1 工作一秒实现,开释锁
pool-1-thread- 2 期待 2s
pool-1-thread- 2 取得锁胜利,开始工作
pool-1-thread- 2 工作一秒实现,开释锁
下面只是简略的实现了 redis 分布式锁的次要逻辑,外面还有很多有余的中央,比方开释锁的时候,咱们先是查问值再比拟,最初再删除,这就不能保障原子性了,这种状况能够应用 lua 脚本来实现,这里就不过多介绍。
在实在工作中,如果要应用 redis 分布式锁,根本都是应用开源框架 redisson 来实现。
Redisson 实现分布式锁
应用 redisson 来实现分布式锁就很简略了,外面实现的细节框架都曾经帮咱们实现好了。
首先引入 redisson 包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.5</version>
</dependency>
在本地同样应用多线程来模仿分布式环境
public class RedissonTest {
private static RedissonClient redissonClient;
static {Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
}
public static void main(String[] args) {for (int i = 0; i < 2; i++) {new Thread(() -> {RLock lock = redissonClient.getLock("redisson:lock");
lock.lock();
System.out.println(Thread.currentThread().getName()+"获取到锁");
try {System.out.println(Thread.currentThread().getName()+"操作");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"操作完结");
}catch (Exception e){e.printStackTrace();
}finally {System.out.println(Thread.currentThread().getName()+"开释锁");
lock.unlock();}
}).start();}
}
}
输入:Thread- 1 获取到锁
Thread- 1 操作
Thread- 1 操作完结
Thread- 1 开释锁
Thread- 2 获取到锁
Thread- 2 操作
Thread- 2 操作完结
Thread- 2 开释锁
总结
在分布式环境下,如果要应用分布式锁,须要联合本身业务需要来抉择适合的分布式锁;咱们都晓得,分布式中有个 CAP 准则,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance);个别分布式只能保障两点,AP 或者 CP, 如果零碎最求一致性,那么就抉择 zookeeper 分布式锁,如果零碎最求可用性,那么就抉择 redis 分布式锁;
<center> 扫一扫,关注我 </center>