redis(八):redis实现分布式锁的两种方式

在分布式系统中,如果多个节点同时操作同一个数据,会造成数据不一致的问题。和多个线程对共享变量进行操作遇到的问题一样。

在java多线程中,一般会对操作共享数据的代码进行加锁,java提供了synchronized关键字可以很方便实现代码加锁。而在分布式系统中,有三种方式实现分布式锁:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁

1. redis分布式锁第一种方式

使用setnx和expire命令实现。

setnx:如果key不存在则设置value并返回1,如果key存在则不进行任何操作返回0

	 class RedisLock1{
		 private Jedis jedis;
		 private String lockKey;
		 private int expireTime;
		 
		 {
			 jedis=new Jedis("localhost");
			 lockKey="lockKey";
			 expireTime=2000;
		 }
		 
		/**
		 * 获取锁(传统方式)
		 * @return 是否获取到锁
		 */
		public  boolean getLock1(){
			String lockValue=String.valueOf(System.currentTimeMillis()+expireTime);
			Long result=jedis.setnx(lockKey, lockValue);
			// 如果当前锁不存在,返回加锁成功
			if(result==1){
				return true;
			}
			
			//如果当前锁不存在,分以下几种状况:
			Long currentLockValue=Long.parseLong(jedis.get(lockKey));
			
			//锁超过了过期时间却仍然存在。造成原因:获取锁后执行逻辑时,程序节点意外崩溃,没有执行到expire或del命令
			if(currentLockValue<System.currentTimeMillis()){
		        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
		        String oldValueStr = jedis.getSet(lockKey, (System.currentTimeMillis()+expireTime)+"");
		        Long oldValueLong=Long.parseLong(oldValueStr);
		        //同一个程序节点,在多线程情况下,这两个值可能不相等
		        if (oldValueStr != null && oldValueLong==currentLockValue) {
		            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
		            return true;
		        }
			}
			//其他情况下,都是没有获取锁
			return false;
		}
		
		/**
		 * 解锁(传统方式)
		 * 存在问题:如果过期时间过短,逻辑还没有执行结束,锁已经过期被删除。如果其他程序节点获取到了锁。这时逻辑执行结束后就会删除其他节点的获取的锁,
		 */
		public  void unLoc1k(){
			/**
			 * 设置过期时间,防止程序崩溃,造成死锁(事实上,有可能程序崩溃,这一步都没执行到,所以需要在获取锁时进行额外判断,判断锁是否已经过期释放。
			 * 如果锁存在且根据值判断已经过了过期时间, 则表明解锁时程序崩溃,没有执行到expire命令,需要重新设置锁)
			 */
			jedis.expire(lockKey, expireTime/1000);
			
			//TODO 获取到锁之后需要执行的逻辑(可以采用AOP的方式)
			
			//执行完逻辑,无论锁是否过期删除,都需要手动删除一遍
			jedis.del(lockKey);
			jedis.close();
		}
	}

缺点:

  • 因为各个节点都设置了过期时间,所以各个节点的时间必须同步
  • 不能保证加锁和解锁是同一个节点,有可能会删除其他节点获取的锁

2. redis分布式锁第二种方式

使用 jedis.set(String key, String value, String nxxx, String expx, int time) 实现加锁

	 class RedisLock2{
		 private Jedis jedis;
		 private String lockKey;
		 private int expireTime;//key过期时间,单位为秒
		 
		 {
			 jedis=new Jedis("127.0.0.1");
			 lockKey="lockKey2";
			 expireTime=2;
		 }
		 
		 /**
		  * 获取分布式锁
		  * @param lockValue 锁的值
		  * @return 是否获取成功
		  */
		 public  boolean getLock2(String lockValue) {
			 /**
			  * 第一个参数是锁的key
			  * 第二个参数是锁的value,为了防止解锁时,解了其他成程序节点的锁,因此每个节点获取锁时,lockValue应该唯一,比如使用UUID的值
			  * 第三个参数设置的是“NX”,意思是SET IF NOT EXIST,即当key不存在时,进行set操作;若key已经存在,则不做任何操作;
			  * 第四个参数设置的是“PX”,设置过期时间标志,具体值由第五个参数决定
			  * 第五个参数是锁的过期时间,单位为秒
			  */
			 String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);

			 //锁设置成功,则返回"OK"
			 if ("OK".equals(result)) {
				 return true;
			 }
			 return false;

		 }

		 /**
		  * 释放分布式锁(通过lua语句)
		  * 在lua语句中实现锁值比较和删除,一个lua命令是原子操作,不会出现误删其他节点锁的情况
		  * @param lockValue 锁的值
		  * @return 是否释放成功
		  */
		 public  boolean unLockByLua(String lockValue) {
			 //TODO 获取到锁之后需要执行的逻辑

			 Long SUCCESS = 1L;
			 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
			 Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));

			 if (SUCCESS.equals(result)) {
				 return true;
			 }
			 return false;

		 }

		 /**
		  * 节点获取锁执行完逻辑后,通过锁值判断是不是该节点加的锁,如果是则删除锁
		  * 存在问题:判断和删除不是原子操作,仍有可能判断成功后,删除其他的节点的key,虽然可能性很小
		  * @param lockValue 传入的锁的值
		  * @return
		  */
		 public  boolean unLock2(String lockValue){
			 //TODO 获取到锁之后需要执行的逻辑

			 // 判断加锁与解锁是不是同一个节点
			 if (lockValue.equals(jedis.get(lockKey))) {
				 jedis.del(lockKey);
				 jedis.close();
			 } 
			 return false;
		 }
	 }
2.1测试

这里使用线程模拟分布式的节点。

public class RedisConnection {
	private  int count=0;
	public void incre(){
		count++;
		System.err.println(count);
	}

	
	public static void main(String[] args){
		RedisConnection connection=new RedisConnection();
		
		for (int i = 0; i <10; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					RedisLock2 lock2=connection.new RedisLock2();
					String lockValue=UUID.randomUUID().toString();
					if(lock2.getLock2(lockValue)){
						//执行逻辑
						connection.incre();
						lock2.unLockByLua(lockValue);
					}else{
						System.err.println("没有拥有锁");
						try {
							int i=10;
							while(i>0){
								Thread.sleep(10);
								i--;
								if(lock2.getLock2(lockValue)){
									//执行逻辑
									connection.incre();
									lock2.unLockByLua(lockValue);
									break;
								}
							}
							
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}	
					
				}
			}).start();
		}
	}
	}

执行结果:

1
没有拥有锁
没有拥有锁
没有拥有锁
没有拥有锁
没有拥有锁
2
3
没有拥有锁
4
5
6
7
8
9
10

这里启动了10个线程模拟10个节点,如果节点没有获取到锁,那么休眠10毫秒,再次获取锁,周而复始(10次机会,轮询次数视具体情况而定)

参考文章:Redis分布式锁的正确实现方式

感谢作者!

3.Redisson实现分布式锁

参考:

Redisson实现Redis分布式锁的N种姿势

高性能分布式锁-redisson的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值