这期内容当中小编将会给大家带来有关Redis中怎样实现分布式锁,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。
在Java里面,如果多个线程同时访问公共资源,不做些同步措施可能就会对数据的一致性造成破坏,例如下面的例子,多个线程同时对COUNT做 +1操作(为了测试方便就使用单机多个线程执行)
import lombok.SneakyThrows;import java.util.concurrent.Semaphore;public class ThreadLockTest extends Thread{// 多个线程对COUNT进行操作 public static volatile int COUNT = 0; // 信号灯,用于所有线程执行完后主线程输出 public static Semaphore semaphore = new Semaphore(0); @SneakyThrows @Override public void run() {for (int i = 0; i < 10; i++) { System.out.println("线程:" + Thread.currentThread().getName() + " 操作:" + COUNT++); Thread.sleep(100); }try { System.out.println("线程:" + Thread.currentThread().getName() + " 执行完成"); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }public static void main(String[] args) throws InterruptedException {new ThreadLockTest().start(); new ThreadLockTest().start(); new ThreadLockTest().start(); semaphore.acquire(3); System.out.println("主线程输入总数:" + COUNT); } }
3个线程,每个线程循环执行 "i+1" 10次,预期输出结果应该是30,输出结果不一定是30,这是因为没同步处理,对一致性造成破坏
不做同步处理会造成结果与我们期望的不一样,所以需要用到synchronized关键字或者Lock对象来做同步处理,但是这两者都只适用于单机的服务器。在分布式服务器下,无法使用synchronized关键字和Lock对象进行同步操作,这个时候就要想想其他办法,Redis就提供了一些分布式锁的方案。如下:
Redis提供呢NX函数给我们设置锁,就是向Redis插入一个key-value,key就是锁名称,value可以使用UUID或者 机器号+线程名称,当用完的时候再删除这个key,但是只有value一致的情况下才能删除,这样才能保证加锁与解锁的线程是同一个,SETNX是Redis提供的一个命令,当Redis中不存在指定的key时才能成功插入,Jedis也有对应的函数。
public static long lock(String key, String requestId){return jedis.setnx(key, requestId); // 获取锁,返回1则获取锁成功,返回0则获取锁失败}
这里顺便把解锁的函数贴出来,这里使用到lua脚本直接判断删除。如果不用lua脚本删除我们的程序要分成几部: 1.根据key查询值;2.判断值与当前线程值是否一致;3.删除key。但是这几部走下来,如果中间出了什么问题就会造成一些麻烦(此处下面再展开可能会有哪些麻烦)。所以为了保证原子性,用lua一步到位。
//删除key的lua脚本,先比较requestId是否相等,相等则删除,使用脚本执行保证原子性private static final String DEL_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";public static long unLock(String key, String requestId) {//删除成功表示解锁成功,返回1则解锁成功,返回0则解锁失败 return jedis.eval(DEL_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId));}
再执行一下代码加上锁之后的执行结果
@SneakyThrows@Overridepublic void run() {for (int i = 0; i < 10; i++) { String uuid = UUID.randomUUID().toString(); while(1 != JedisTest.lock("COUNT_ADD_LOCK", uuid)); System.out.println("线程:" + Thread.currentThread().getName() + " 操作:" + COUNT++); Thread.sleep(100); JedisTest.unLock("COUNT_ADD_LOCK", uuid); }try { System.out.println("线程:" + Thread.currentThread().getName() + " 执行完成"); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }
执行结果也每次都能保证输出的是我们的预期值
上面的加锁方案会有个问题,就是当加锁的那个线程所在的服务器宕机之后,锁就一致存在,会导致其他线程一直在阻塞,所以需要加上一个失效时间。当加锁的服务器宕机之后过了过了有效时间其他服务器就能再上锁。
加失效时间
private static final int LOCK_EX_SECONDS = 5;public static long lock(String key, String requestId){return jedis.setex(key,LOCK_EX_SECONDS,requestId);}
这里往Redis插入数据的时候必须同时加上过期时间,如果分成两步走:
1.插入数据,
2.给key设置失效时间
有可能在第一步插入数据之后服务器宕机了,这样就造成失效时间没有设置上,死锁的问题就又出现了,所以必须插入数据的时候同时设置失效时间。
给key加了失效时间比没有好,但是缺陷还是有很多,因为失效时间都是根据我们开发人员评估这段业务执行起来需要多少时间,并不是准确计算每次请求这段业务所需的时间。这样就会造成一个问题:我们业务还没执行完,锁因为时间到期而被释放,再有其他线程同时访问到这部分公共资源,也会破坏其一致性。还有一个就是上面提到解锁操作那3步(1.根据key查询值;2.判断值与当前线程值是否一致;3.删除key)不能分开执行的原因,例如:
线程A:lock(),插入数据并且设置过期时间
线程A:执行业务完毕
线程A:根据锁的key获取value--解锁第一步
线程A:判断当前线程的value与锁的value一致--解锁第二部
上面判断结果一致情况下,key这个时候刚好过了有效时间
线程B:lock(),获取锁成功
线程A:unlock() 解锁成功,因为上面已经判断了value一致,所以只需要del(key)就能解锁
从上面这个流程看到,如果解锁不保证原子性可能会出现线程A把线程B的锁释放的问题。
还有锁失效的问题需要如何处理?
Redisson
可以使用Redisson来实现锁,Redisson具有一个看门狗机制,Redisson实现锁的方案是给锁设置一个过期时间,当业务还没执行完Redisson会更新一下锁的失效时间,如果发生宕机情况锁的有效时间过了自然就释放了,而且Redisson是可重入锁。下面看一下Redisson加锁解锁代码
public class RedissonConfig {public static final String REDIS_ADDRESS_PORT = "redis://192.168.0.90:6380"; public static RedissonClient REDISSON_CLIENT; public static synchronized RedissonClient getRedisson(){if(REDISSON_CLIENT == null) { Config config = new Config(); SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setAddress(REDIS_ADDRESS_PORT); REDISSON_CLIENT = Redisson.create(config); }return REDISSON_CLIENT; } }
@SneakyThrows@Overridepublic void run() {for (int i = 0; i < 10; i++) { RLock lock = RedissonConfig.getRedisson().getLock("COUNT_ADD_LOCK"); while(!lock.tryLock()); System.out.println("线程:" + Thread.currentThread().getName() + " 操作:" + COUNT++); Thread.sleep(100); lock.unlock(); }try { System.out.println("线程:" + Thread.currentThread().getName() + " 执行完成"); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }
多次的执行结果都能达到预期值
上述就是小编为大家分享的Redis中怎样实现分布式锁了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。