前言本文主要深入讲解redis分布式锁的原理与实现。以后会有redis分布式锁相关的文档齐全的问题。一、背景为什么需要分布式锁?当多个线程同时操作同一个资源时,我们通常使用,比如synchronized来保证同一时刻只有一个线程获取到对象锁,然后对资源进行处理。在分布式条件下,每个服务都是独立部署的。这时候锁服务的对象就变成了当前的应用服务,也就是其他服务仍然可以执行这段代码,导致服务出现问题。如果我们希望多个服务独立部署的时候,能够控制资源的单独执行,就需要引入分布式锁来解决这种场景。什么是分布式锁?分布式锁。它是一种锁的实现,控制分布式系统中的不同进程共同访问同一个共享资源。分布式锁可以理解为“总部”的概念。总部控制锁的占有并通知其他服务等待。每个独立部署从总部获取锁定信息,从而避免地方政治的可能性。分布式锁就是这样的思想。我们可以使用第三方组件(如redis、zookeeper、database)来监控全局锁,控制锁的持有和释放。2.分布式锁原理及使用setnx是[setifnotexists]的缩写。当且仅当键不存在时,将键的值设置为值,如果给定的键已经存在,则setnx将不执行任何操作。下面我们通过这个命令来看一个简单的分布式锁使用案例,了解其内部原理。2.1分布式锁的演进:Phase1代码demo如下:booleanlock=redisTemplate.opsForValue().setIfAbsent("lock","111");if(lock){//锁定成功Map>stringListMap=getStringListMap();redisTemplate.delete("lock");//删除锁returnstringListMap;}else{//锁失败:重试(自旋锁)returngetCatalogJsonFromRedis();}复制代码问题:this是最原始的锁使用问题,但是这里明显有问题。如果锁被占用后,程序在执行业务的过程中崩溃了,此时锁会一直存在于redis中。解决办法:设置锁的过期时间,即使没有删除也会自动删除。2.2分布式锁的演进:Phase2代码demo如下:booleanlock=redisTemplate.opsForValue().setIfAbsent("lock","111");如果(锁){redisTemplate.expire(“锁”,30,TimeUnit.SECONDS);//锁定成功Map>stringListMap=getStringListMap();redisTemplate.delete("lock");//删除锁returnstringListMap;}else{//锁失败:重试(自旋锁)returngetCatalogJsonFromRedis();}复制代码问题:设置了setnx,然后设置过期时间,中间会出现宕机,也会出现死锁。解决方案:设置过期时间和占位符必须是原子的。Redis支持使用setnxex命令。2.3分布式锁的演进:Phase3booleanlock=redisTemplate.opsForValue().setIfAbsent("lock","111",30,TimeUnit.SECONDS);if(lock){//锁定成功Map>stringListMap=getStringListMap();redisTemplate.delete("lock");//删除锁returnstringListMap;}else{//锁失败:重试(自旋锁)returngetCatalogJsonFromRedis();使用一个命令完成此锁定操作。问题:锁是直接删除吗?(如果业务很忙,锁本身过期了,我们直接删除,可能会删除别人持有的锁)解决方法:加锁的时候,指定值为uuid,每个人只有匹配自己的才会删除自己的锁。2.4分布式锁进化:阶段4Stringuuid=UUID.randomUUID().toString();布尔锁=redisTemplate.opsForValue().setIfAbsent("lock",uuid,30,TimeUnit.SECONDS);if(lock){//锁定成功Map>stringListMap=getStringListMap();StringlockValue=redisTemplate.opsForValue().get("lock");if(uuid.equals(lockValue)){redisTemplate.delete("lock");//删除锁}returnstringListMap;}else{//加锁失败:重试(自旋锁)returngetCatalogJsonFromRedis();复制代码还是有问题。删除其他线程的值还是可以的为什么?如果我们从redis中获取锁的值,并通过相等校验,进入删除锁的逻辑,但是此时锁过期自动删除,另外一个线程进入占用锁,那么就有出问题了,此时删除的锁是别人的锁。道理和前面一样:比较锁和删除值的时候,不是原子操作。解决方法是使用lua脚本删除。2.5分布式锁的演进:Phase5使用lua脚本删除锁。字符串uuid=UUID.randomUUID().toString();布尔锁=redisTemplate.opsForValue().setIfAbsent("lock",uuid,30,TimeUnit.SECONDS);if(lock){Map>stringListMap;try{//锁定成功stringListMap=getStringListMap();}finally{//定义lua脚本Stringscript="ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0结尾”;//原子删除Integerlock1=redisTemplate.execute(newDefaultRedisScript(script,Integer.class),Arrays.asList("lock",uuid));}返回字符串列表映射;}else{//加锁失败:重试(自旋锁)returngetCatalogJsonFromRedis();}复制代码3.分布式锁Redission3.1Redission简介Redisson是在Redis的基础上实现的Java内存数据网格(In-MemoryDataGrid),它不仅提供了一系列分布式的通用Java对象,还提供了许多分布式服务。这些包括(BitSet,Set,Multimap,SortedSet,Map,List,Queue,BlockingQueue,Deque,BlockingDeque,Semaphore,Lock,AtomicLong,CountDownLatch,Publish/Subscribe,Bloomfilter,Remoteservice,Springcache,Executorservice,LiveObjectservice,Schedulerservice)Redisson提供了最简单、最方便的Redis使用方式。Redisson的目的是促进用户对Redis的关注点分离(SeparationofConcern),让用户更专注于处理业务逻辑。Redission是redis官方推荐的客户端。它提供RedLock锁。RedLock继承自juc的Lock接口。提供中断、超时、尝试获取锁等操作,支持重入、互斥等特性。3.2使用导入依赖org.redissonredisson3.15.2复制代码配置:以下是官网提供的参考配置://默认连接地址127.0.0.1:6379RedissonClientredisson=Redisson.create();//ConfigurationConfigconfig=newConfig();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClientredisson=Redisson.create(config);复制代码Configconfig=newConfig();config.useClusterServers().setScanInterval(2000)//集群状态扫描间隔,单位毫秒//可以使用"rediss://"启用SSL连接.addNodeAddress("redis://127.0.0.1:7000","redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");RedissonClientredisson=Redisson.create(config);语言:cpp3.3测试分布式锁@ControllerpublicclassTestRedissonClient{@AutowiredRedissonClientredisson;@ResponseBody@GetMapping("/hello")publicStringhello(){//1.获取一把锁,只要锁的名字相同,就是同一把锁RLocklock=redisson.getLock("my-lock");//2.加锁lock.lock();//等待块类型//模拟超长等待Thread.sleep(20000);}catch(Exceptione){e.printStackTrace();}finally{//3.UnlockSystem.out.println("释放锁..."+Thread.currentThread().getId());锁定.解锁();}return"hello";}}Copycoderedission解决了两个问题:1.自动续锁。如果业务时间过长,运行时会自动加锁新的30s。不用担心锁时间过长锁会自动过期被删除丢失。2、对于加锁的业务,只要操作完成,当前的锁就不会被续订。即使不手动解锁,默认也会在30s后删除锁(当前线程销毁前会调用lock方法)。如果超时时间传给了锁,它就会发送到redis去执行占用锁的脚本。默认的超时时间就是我们设置的时间。如果不指定超时时间,则使用门狗的默认时间。(30*1000)只要锁被成功占用,就会启动一个定时任务【重新设置锁的过期时间,新的过期时间为看门狗的默认时间】,每10秒刷新一次,并且更新时间已满。internaLockLeaseTime[看门狗时间]/310s。最好的实战,【推荐写作】加点时间省更新时间。//10s自动解锁,指定时间必须大于业务时间(否则会报错,不确定不要使用)lock.lock(10,TimeUnit.SECONDS);复制代码3.4读写锁写模式下一次只能有一个线程占用读写锁,但读模式下多个线程可以同时占用读写锁。读写锁适用于对数据结构的读次数大于写次数的情况,因为读模式锁可以共享,而写模式锁是独占的,所以读写锁也称为共享排他锁。保证可以读取到最新的数据。修改时,写锁是独占锁。读锁是共享锁。如果写锁没有释放,则必须等待写+读:等待写锁写+读:等待写锁释放。Write+write:阻塞模式read+write:有读锁。写作也需要等待。只要有写,就必须等待。3.5缓存一致性问题如何使缓存中的数据与数据库保持一致。3.5.1更改双写模式数据库后更改缓存。问题:会有脏数据。方案一:加锁方案二:如果允许延迟(今天更新的数据明天显示,或者延迟几分钟或几小时),设置过期时间(推荐)3.5.2故障模式数据库变更后,删除缓存,等待下一个活动查询,然后更新。问题:会出现读写,脏数据解决方案一:加锁解决方案二:写频繁读少,直接操作数据库,去掉缓存层比较好。3.5.3解决方案(过期时间+读写锁)无论是双写模式还是失败模式,都会导致缓存不一致。也就是说,如果同时更新多个实例,就会发生一些事情。我应该怎么办?(1)如果是用户纬度数据(订单数据,用户数据),并发的几率很小,所以不用担心这个问题,缓存数据加上过期时间,每隔一段时间触发readactiveupdate(2)如果是菜单、产品介绍等基础数据,也可以使用canal订阅binlog。(3)缓存数据+过期时间也足以满足大部分业务对缓存的需求。(4)通过加锁保证并发读+写,写+写时按顺序排队。读不读都没关系。所以适合使用读写锁。(业务与核心数据无关,允许忽略临时脏数据)总结:(1)我们可以放在缓存中的数据,实时性和一致性要求不高。所以在缓存数据的时候,加上一个过期时间,保证每天都能拿到最新的数据。(2)不要过度设计,增加系统的复杂度(3)遇到对实时性和一致性要求高的数据时,要检查数据库,即使速度较慢。对于缓存数据的一致性问题,我们可以使用Cannal来完成这个场景。一般用于大数据项目