前言
上回说到如何使用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.clientsgroupId>
<artifactId>jedisartifactId>
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(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.redissongroupId>
<artifactId>redissonartifactId>
<version>3.11.5version>
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分布式锁;
![cc9373000085646bf84a0f5d602c5527.png](https://i-blog.csdnimg.cn/blog_migrate/edd26a9b68d2d70e89a63418faaa2ade.jpeg)