分布式锁顾名思义就是在分布式系统下的锁,而使用锁的唯一目的就是为了防止多个请求同时对某一个资源进行竞争性读写
在使用多线程时,为了让某一资源某一时刻只能有一个操作者,经常使用synchronized,这点大家都很熟悉
那什么时候使用分布式锁?
当一套项目只部署一套的时候,使用synchronized就可以了,但是当同一套项目部署了多套,即进行分布式部署时,
假设部署了同样的A,B,C三套系统,系统里面有一个操作同一时刻只允许一个用户进行操作,如上所说,只部署一套时,用synchronized限定可以达到要求
现在部署三套之后,如果 a1,b1,c1三个甚至更多用户来同时访问ABC三套系统中只能有一个人操作的方法时,则都可以进行操作。synchronized是不是没达到设计效果
所以:
只有当项目进行分布式部署且有限定不能同时操作的资源时,才会使用分布式锁。
明确了啥时候用,那么该如何用,怎么设计?
按照在学java之初的思路,应该设置一个全局的标识,假设为 flag = true
如果某一时刻某个线程来获取的时候,发现是true,就表示该线程获取到锁了,并改为false,其它线程来发现是false就等一段时间再试,获得锁的线程执行完了,修改为false以便其它线程使用
那么问题来了:
1.怎么存储这个全局的flag,因为要频繁的读取修改
2.怎么保证同一时刻只有一个线程取得锁, 如果两个线程同时来判断flag,都发现是true,那么两个线程都获取到锁了,达不到目的
带着这两个问题,正式步入正题:
由于需要频繁的读取,而存储的值很简单,则考虑使用缓存,而redis就相当符合需要,redis可以达到每秒100,000次的读写,而且可以供多个项目同时操作
存储问题解决了,那么怎么保证同一时刻只有一个线程获取到锁,这就需要用到redis相关的命令了
redis中有一个setnx(set if not exist)命令,表示如果没有这个key就设值并返回1,如果key已经存在则返回0
由于redis是单线程单进程的基于内存操作的工具,所以同一时刻只会有一个命令执行成功。
所以,可以简单的使用setnx命令来进行锁的获取,如果返回的是1,表示获取到了锁,就开始执行业务逻辑,完成之后删除key,其它线程才可以获取到锁
但是问题又来了,如果已经获取到锁的线程由于执行出错等原因,一直不释放锁(delete key),那么其它线程则永远也无法获取到锁,这就和死锁一样吧
所以,释放锁的策略很重要
redis 有一个expire命令,可以让key在一定时间后失效(自动删除),但如果成功设置了key但expire来没设置成功时服务就挂了,并且程序又执行出错死锁了一直不释放锁怎么办?
这时就需要其它线程来进行解锁,其它线程解锁的判断条件就至关重要,必须明确啥时候可以解锁
参考了很多相关文章发现其中一种比较好的策略:
redis中的value设置为 当前当前时间+失效时间,使用setnx命令成功获取锁后,执行任务,如果执行成功,则删除key,如果执行失败,导致锁不释放,则由其它线程来释放锁
当其它线程通过get key 获取到时间发现已经超时了,则可以进行锁的获取,
其它线程通过使用getset命令来对key进行设置,如果返回的值(旧值) 等于 自己发送过去设置的值(新值),则表示当前线程获取到了锁,如果不一致,则表示其它线程获取到了锁,
疑问来了,如果getset执行成功了,但是返回的值和该线程设置的值不一致,会不会影响其它线程? 不会哈,因为这个时间改动范围是很小很小的,可以忽略了
各个服务器时间一定要同步哦
以上就是基本的实现思路了
在自己实现过程中,发现了一个较好的开源项目,也是基于redis, 地址:https://github.com/redisson/redisson/wiki
并且可以快速的和springmvc等框架集成
下面红色标注的就是我在集成过程中遇见的问题,一定要小心
1.pom中对jar包进行引入(jedis相关jar包也要引入哦)
1 <!--Redisson --> 2 <dependency> 3 <groupId>org.redisson</groupId> 4 <artifactId>redisson</artifactId> 5 <version>3.4.3</version> 6 <exclusions> 7 <exclusion> 8 <groupId>org.slf4j</groupId> 9 <artifactId>slf4j-api</artifactId> 10 </exclusion> 11 <exclusion> 12 <groupId>com.fasterxml.jackson.core</groupId> 13 <artifactId>jackson-databind</artifactId> 14 </exclusion> 15 <exclusion> 16 <groupId>com.fasterxml.jackson.core</groupId> 17 <artifactId>jackson-core</artifactId> 18 </exclusion> 19 </exclusions> 20 </dependency>
2.增加spring集成配置文件 applicationContext-redission.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:redisson="http://redisson.org/schema/redisson" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://redisson.org/schema/redisson classpath:org/redisson/spring/support/redisson-1.1.xsd"> <redisson:client id="redissonClient"> <redisson:single-server address="${redis.address}"/> </redisson:client> <!-- <redisson:client> --> <!-- <redisson:cluster-servers> --> <!-- <redisson:node-address value="redis://192.168.0.32:7000" /> --> <!-- <redisson:node-address value="redis://192.168.0.32:7001" /> --> <!-- <redisson:node-address value="redis://192.168.0.32:7002" /> --> <!-- <redisson:node-address value="redis://192.168.0.32:7003" /> --> <!-- <redisson:node-address value="redis://192.168.0.32:7004" /> --> <!-- <redisson:node-address value="redis://192.168.0.32:7005" /> --> <!-- </redisson:cluster-servers> --> <!-- </redisson:client> --> </beans>
注意address值格式为: redis://ip:port 如: redis://192.168.0.32:6379
classpath:org/redisson/spring/support/redisson-1.1.xsd 注意定义文件是这么引入的,开源wiki里面可不是这么写的,小坑了我一下 3.使用就简单了,我简单写了一个demo进行了测试,同时也可以部署两套,同时发起请求进行测试
1 @Autowired 2 private RedissonClient redissonClient; 3 4 private static AtomicInteger st = new AtomicInteger(0); 5 6 @RequestMapping("/test/redission") 7 public void test() { 8 st.getAndSet(0); 9 for(int i=0;i<=9999;i++){ 10 new Thread(new Runnable() { 11 @Override 12 public void run() { 13 test(String.valueOf(st.getAndIncrement())); 14 } 15 }).start(); 16 } 17 18 } 19 20 public void test(String value) { 21 RLock rLock = redissonClient.getLock("anyLock"); 22 boolean res = false; 23 try { 24 res = rLock.tryLock(200, 10, TimeUnit.SECONDS); 25 if (res) { 26 System.out.println(String.format("%04d",Integer.valueOf(value))); 27 //System.out.println("开始执行业务:" + value + ", " + Thread.currentThread().getName() + ", " + format.format(new Date()) + ", 取得的值为:" + String.valueOf(value)); 28 if(Integer.valueOf(value) % 1000 == 0){ 29 Thread.sleep(1000); 30 } else { 31 //Thread.sleep(1000); 32 } 33 //System.out.println("业务执行结束:" + value + ", " + Thread.currentThread().getName() + ", " + format.format(new Date())); 34 } else { 35 System.out.println("not lock:"+String.format("%04d",Integer.valueOf(value))); 36 } 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } finally { 40 if(rLock.isHeldByCurrentThread()){ 41 rLock.unlock(); 42 } 43 } 44 }
启动多个线程模拟并发访问,根据值来进行区分,同时可以调整超时时间来进行测试锁超时时的情况,具体使用参照:https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
在我进行压力测试过程中发现,使用公平锁效率要低很多,其它的锁暂时还没进行过压力测试,不知道具体情况、
但具体业务中,肯定是不同业务使用不同的锁,千万不要整个系统中不同的业务都使用一个锁哈,只要不相互关联就要完全分开
以上纯属个人思路,有错误的地方敬请指正