前言在Java中,我们对锁比较熟悉。常用的有synchronized和Lock锁。在java并发编程中,我们使用锁来实现当多个线程竞争时同一个共享资源或变量导致的数据不一致的问题,但是JVM锁只能针对单个应用服务。随着我们业务的发展,单机部署的系统已经演化为分布式系统。由于在分布式系统中,此时JVM锁的并发控制没有作用。为了解决跨JVM锁,控制对共享资源的访问,分布式锁诞生了。什么是分布式锁?分布式锁是一种控制分布式系统之间共享资源同步访问的方法。在分布式系统中,经常需要协调它们的动作。如果不同系统或同一系统的不同主机共享一个或一组资源,在访问这些资源时往往需要互斥,以防止相互干扰,保证一致性。这种情况下,你需要为什么在分布式JVM锁下不能使用分布式锁呢?下面看一下代码,看看为什么jvm锁在集群下不可靠。我们来模拟抢购商品的场景。服务A有十个用户要抢购这个产品,服务B有十个用户要抢购这个产品。当其中一位用户抢购成功后,其他用户将无法下单购买该商品。那么服务A或服务B会购买吗?看看其中一个用户是否抢货成功,状态会变成1GrabService:publicinterfaceGrabService{/***商品抢单*@paramorderId*@paramdriverId*@return*/publicResponseResultgrabOrder(interorderId,intdriverId);}GrabJvmLockServiceImpl:@服务(“grabJvmLockService”)publicclassGrabJvmLockServiceImplimplementsGrabService{@AutowiredOrderServiceorderService;@OverridepublicResponseResultgrabOrder(intorderId,intdriverId){Stringlock=(orderId+“”);同步(lock.intern()){tryn{(System.out.printl“用户:”+driverId+"执行订单逻辑");booleanb=orderService.grab(orderId,driverId);if(b){System.out.println("User:"+driverId+"下单成功");}else{System.out.println("User:"+driverId+"orderfailed");}}finally{}}returnnull;}}OrderService:publicinterfaceOrderService{publicbooleangrab(intorderId,intdriverId);}OrderServiceImpl:@ServicepublicclassOrderServiceImplimplementsOrderService{@AutowiredprivateOrderMappermapper;publicbooleangrab(intorderId,intdriverId){Orderorder=mapper.selectByPrimaryKey(orderId);try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}if(order.getStatus().intValue()==0){order.setStatus(1);mapper.updateByPrimaryKeySelective(order);returntrue;}returnfalse;}}这里我们模拟集群环境,启动8004和8005两个端口进行访问。这里我们使用jmeter进行测试。不懂jmeter的可以看我之前关于tomcat压测的文章:tomcat优化项目启动顺序:先启动Server-eureka注册中心,再启动8004和8005端口测试结果:8004服务和8005服务都有一个用户成功下单了这个商品,但是只有一个用户可以抢到这个商品,所以如果jvm锁在集群或者分布式下,无法保证只有一个线程可以抢到同时访问共享变量的数据,不能解决分布式和集群环境的问题,所以需要使用分布式锁。分布式锁的三种实现分布式锁的实现有三种:基于数据库的分布式锁基于缓存(Redis)的分布式锁基于Zookeeper的分布式锁今天主要讲一下基于Redis的分布式分布式锁的实现方式有以下三种:reids:1.基于redis的SETNX实现分布式锁2.redisson实现分布式锁3.使用redLock实现分布式锁目录结构:方法一:基于SETNX实现分布式锁value设置为value当且仅当key没有存在。如果给定的键已经存在,SETNX什么都不做。setnx:当key存在时,不做任何操作,key不存在,则设置锁:SETOrderIddriverIdNXPX30000如果上述命令执行成功,则客户端成功获取到锁,即可访问共享资源;而如果上述命令执行失败,则说明获取锁失败。释放锁:关键是判断是不是自己加的锁。GrabService:publicinterfaceGrabService{/***商品抢单*@paramorderId*@paramdriverId*@return*/publicResponseResultgrabOrder(intorderId,intdriverId);}GrabRedisLockServiceImpl:@Service("grabRedisLockService")publicclassGrabRedisLockServiceImplimplementsGrabService{@AutowiredStringRedisTemplatestringRedisTemplate;@AutowiredOrderServiceorderService;@OverridepublicResponseResultgrabOrder(intoorderId,intdriverId){//生成keyStringlock="order_"+(orderId+"");/**情况1,如果直到释放锁才执行,比如业务逻辑执行到一半,则操作然后维护重启服务,或者服务器挂了,最后没有Go,怎么办?*添加超时时间*///booleanlockStatus=stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(),driverId+"");//if(!lockStatus){//returnnull;///**情况2:如果加了超时时间,会出现加不上的情况,运维重启*///booleanlockStatus=stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(),driverId+"");//stringRedisTemplate.expire(lock.intern(),30L,TimeUnit.SECONDS);//if(!lockStatus){//returnnull;//}/**情况三:timeout应该一次性加,不应该加分为2行代码,**/booleanlockStatus=stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(),driverId+"",30L,TimeUnit.SECONDS);if(!lockStatus){returnnull;}try{System.out.println("User:"+driverId+"执行抢单逻辑");booleanb=orderService.grab(orderId,driverId);if(b){System.out.println("User:"+driverId+"抢单成功");}else{System.out.println("User:"+driverId+"Ordergrabfailed");}}finally{/***存在这种释放锁,可能会释放别人的锁*///stringRedisTemplate.delete(lock.intern());/***如下代码避免释放其他人的锁*/if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))){stringRedisTemplate.delete(lock.intern());}}returnnull;}}这里可能有人会问,如果我的业务执行时间超过了锁释放时间怎么办?我们可以使用守护线程,只要我们当前线程还持有锁,当达到10S时,守护线程会自动对该线程进行超时操作,30S的过期时间会一直持续到锁被释放,合同不会续签。一个子线程被启动。原来的时间是N,每隔N/3,继续关注N:key就是我们要锁定的目标,比如订单ID。driverId是我们的产品ID,在足够长的时间段内,它必须在所有客户端的所有锁获取请求中是唯一的。即订单被用户抢了。NX表示只有当orderId不存在时,SET才能成功。这样可以确保只有第一个请求的客户端可以获得锁,在释放锁之前,其他客户端都无法获取锁。PX30000表示锁的自动过期时间为30秒。当然,这里的30秒只是一个例子,客户端可以选择合适的过期时间。此锁必须设置过期时间。否则,当一个客户端获取锁成功后,如果崩溃了,或者因为网络分区,无法再与Redis节点通信,那么它会一直持有锁,其他客户端永远不会获取锁失败。Antirez在后续的分析中也强调了这一点,并将这个过期时间称为锁有效期(lockvaliditytime)。获得锁的客户端必须在这段时间内完成对共享资源的访问。此操作不能拆分。>SETNXorderIddriverIdEXPIREorderId30这两条命令虽然和前面算法描述中的SET命令执行效果一样,但是它们不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE,导致它永远持有锁。造成僵局。方法二:基于redisson实现分布式锁流程图:代码实现:@Service("grabRedisRedissonService")publicclassGrabRedissonServiceImplimplementsGrabService{@AutowiredRedissonClientredissonClient;@AutowiredOrderServiceorderService;@OverridepublicResponseResultgrabOrder(interorderId,intdriverId){//生成keyStringlock="orderId_"+(order");RLockrlock=redissonClient.getLock(lock.intern());try{//这段代码设置key超时时间默认为30秒,10秒后,再延时rlock.lock();System.out.println("User:"+driverId+"执行抢单逻辑");booleanb=orderService.grab(orderId,driverId);if(b){System.out.println("User:"+driverId+"抢单成功");}else{System.out.println("User:"+driverId+"抢单失败");}}finally{rlock.unlock();}returnnull;}}重点:1.redis故障问题。如果redis故障,所有客户端获取不到锁,服务不可用,为了提高可用性,我们为redis配置master-slave,当master不可用,系统切换到slave。由于Redis的主从复制(replication)是异步的,这可能会导致失去锁的安全性。1.Client1从Master处获取锁。2、Master挂了,存储锁的key还没有同步到Slave。3.Slave升级为Master。4.Client2从新的Master那里获取同一个资源对应的锁。客户端1和客户端2同时持有同一资源的锁。锁的安全性被打破了。2.锁有效期设置怎样才合适?如果设置的太短,锁可能在客户端完成对共享资源的访问之前就过期,从而失去保护;如果设置过长,一旦某个持有锁的客户端释放锁失败,其他所有客户端将无法获取锁,从而长时间无法正常工作。它应该设置得稍微短一些。如果线程持有锁,开启线程自动延长有效期。方法三:基于RedLock实现分布式锁。针对以上两点,antirez设计了Redlock算法。Redis的作者Antirez给出了更好的实现。它被称为Redlock,被认为是Redis官方关于实现分布式锁的指导规范。Redlock的算法说明放在Redis官网:https://redis.io/topics/distlock目的:对共享资源做互斥访问。因此,antirez提出了一种新的分布式锁算法Redlock,它是基于N个完整独立的Redis节点(通常N可以设置为5),也就是说N个Redis的数据不能相互通信,类似于几个陌生人的代码实现:@Service("grabRedisRedissonRedLockLockService")publicclassGrabRedisRedissonRedLockLockServiceImplimplementsGrabService{@AutowiredprivateRedissonClientredissonRed1;@AutowiredprivateRedissonClientredissonRedissonRed2;@AutowiredprivateRedissonClientredissonRedissonRed2;@AutowiredprivateRedissonClientredissonRedissonRed2;@AutowiredprivateRedissonClientredissonRedLockLockService";@AutowiredOrderServiceorderService;@OverridepublicResponseResultgrabOrder(intorderId,intdriverId){//generatekeyStringlockKey=(RedisKeyConstant.GRAB_LOCK_ORDER_KEY_PRE+orderId).intern();//红锁RLockrLock1=redissonRed1.getLock(lockKey);RLockrLockget2=redissonRed2.Key);RLockrLock3=redissonRed2.getLock(lockKey);RedissonRedLockrLock=newRedissonRedLock(rLock1,rLock2,rLock3);try{rLock.lock();//这段代码设置按键超时时间默认为30秒,10秒后,再延时System.out.println("User:"+driverId+"执行订单抓取逻辑");booleanb=orderService.grab(orderId,driverId);if(b){System.out.println("User:"+driverId+"抢单成功");}else{System.out.println("User:"+driverId+"抢单失败");}}finally{rLock.unlock();}returnnull;}}运行Redlock算法的客户端为了完成获取锁操作,执行以下步骤:获取当前时间(毫秒)依次对N个Redis节点进行获取锁操作。这个获取操作和前面基于单个Redis节点获取锁的过程是一样的,包括取值driverId和过期时间(比如PX30000,就是锁的有效时间)。为了保证算法在某个Redis节点不可用时能够继续运行,获取锁的操作也有一个超时时间(timeout),这个时间远短于锁的有效时间(几十个量级)毫秒)。客户端从某个Redis节点获取锁失败后,应该立即尝试下一个Redis节点。这里的失败应该包括任何类型的失败,比如Redis节点不可用,或者Redis节点上的锁已经被其他客户端持有,计算整个获取锁的过程需要多长时间。计算方法是用当前Time减去步骤1记录的时间。如果客户端成功从大多数Redis节点(>=N/2+1)获取到锁,例如:如果有5台机器成功加锁,3台被加锁successfullylocked,默认加锁成功,获取锁的总时间不超过锁的时间。生效时间(lockvaliditytime),则客户端认为最终锁获取成功;否则,认为最终获取锁失败。如果最终获取锁成功,那么要重新计算锁的有效时间,与原来的锁相等。有效时间减去步骤3计算的获取锁消耗的时间,如果最终获取锁失败(可能是因为获取锁的Redis节点数小于N/2+1,或者整个获取锁的过程locktakeslongerthantheinitialvalidtimeofthelock),客户端应立即向所有Redis节点发起释放锁的操作(即前面介绍的RedisLua脚本)。上面的描述只是获取锁的过程,释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管此时这些节点是否成功获取锁.综上所述,redis分布式锁就到此为止了。使用哪种类型的分布式锁取决于公司的业务。大流量可以使用RedLock实现分布式锁,小流量可以使用redisson。Zookeeper的实现在后面会讲解分布式锁。
