Redis 分布式锁

0、前言

本文所有代码可见 => 【gitee code demo】
本文涉及的主题:

  1. 为什么使用分布式锁:单机锁在集群中会失效
  2. 分布式锁的特征 & 手写redis分布式锁
  3. redisson

1、单机锁在集群失效问题演示

使用nginx 搭建集群服务

直接启动,获取初始文件

docker run -p 80:80 –name nginx -d nginx:1.10

复制要挂载的文件到宿主机

docker cp cf15f5516cc0:/etc/nginx/ /usr/local/nginx/conf/
docker cp cf15f5516cc0:/var/log/nginx /usr/local/nginx/logs/
docker cp cf15f5516cc0:/usr/share/nginx/html /usr/local/nginx/html/

路径结构如下

在这里插入图片描述

修改配置文件

/usr/local/nginx/conf/nginx.conf

/usr/local/nginx/conf/conf.d/default.conf

upstream 命名不支持_,改为任意合法名称即可。例如:nginx-redis

在这里插入图片描述

配置负载均衡到 两个业务端口

在这里插入图片描述

删除原容器,启动新容器

docker rm $(docker stop nginx)


docker run -p 80:80 --name nginx  \
 -v /usr/local/nginx/html:/usr/share/nginx/html \
 -v /usr/local/nginx/logs:/var/log/nginx \
 -v /usr/local/nginx/conf/:/etc/nginx \
 -d nginx:1.10

jmeter 测试,出现了重复消费问题

redis库存: set inventory001 100

测试计划:lock锁集群.jmx

集群服务:端口 7777、8888

解释

synchronized 和 lock 只对同一个进程下的多线程有效,多进程不管用(多进程是多个锁)

在这里插入图片描述

2、分布式锁

独占性 setnx

public String sale() {
	String key = "RedisLock";
	String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
	// setnx <key, value>
	while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
		//暂停20毫秒,类似CAS自旋
		try {
			TimeUnit.MILLISECONDS.sleep(20);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	try {
		// 业务代码
	} finally {
		stringRedisTemplate.delete(key);
	}
}

超时释放 expire

public String sale() {
	String key = "RedisLock";
	String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
	
	// setnx <key, value>
	// 原子性设置过期时间,防止宕机后出现死锁
	while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
		//暂停20毫秒,类似CAS自旋
		try {
			TimeUnit.MILLISECONDS.sleep(20);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	try {
		// 业务代码
	} finally {
		stringRedisTemplate.delete(key);
	}
}

安全性

唯一标识 requestId = uuid:threadId

public String sale() {
	String key = "RedisLock";
	String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
	
	// setnx <key, value>
	// 原子性设置过期时间,防止宕机后出现死锁
	while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
		//暂停20毫秒,类似CAS自旋
		try {
			TimeUnit.MILLISECONDS.sleep(20);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	try {
		// 业务代码
	} finally {	
		// 添加校验逻辑 防止误删
		if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)) {
			stringRedisTemplate.delete(key);
		}
	}
}

自动续期:业务没执行完之前,自动续期,防止提前删除,别的线程

private void renewExpire() {
	String script =
			"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
					"return redis.call('expire',KEYS[1],ARGV[2]) " +
					"else " +
					"return 0 " +
					"end";
	
	new Timer().schedule(new TimerTask() {
		@Override
		public void run() {
			if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
				renewExpire();
			}
		}
	}, expireTime/3);
}

原子性 lua脚本保证原子性

public String sale() {
	String key = "RedisLock";
	String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
	
	// setnx <key, value>
	// 原子性设置过期时间,防止宕机后出现死锁
	while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
		//暂停20毫秒,类似CAS自旋
		try {
			TimeUnit.MILLISECONDS.sleep(20);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	try {
		// 业务代码
	} finally {
		// lua 脚本保证原子性
		String delLuaScript = "if (redis.call('get', keys[1]) == args[1]) then return redis.call('del', key[1]) else return 0 end";
		stringRedisTemplate.execute(RedisScript.of(delLuaScript, Boolean.class), Collections.singletonList(key), uuidValue);
	}
}

可重入性 hash(增加一个加锁次数的信号量)

redis.call(‘hexists’,KEYS[1],ARGV[1]) == 1 : 线程已经获取了锁

public String sale() {
	String lockScript =
		"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
				"then " +
				"   redis.call('hincrby',KEYS[1],ARGV[1],1) " +
				"   redis.call('expire',KEYS[1],ARGV[2]) " +
				"   return 1 " +
				"else " +
				"   return 0 " +
				"end";
	while (!stringRedisTemplate.execute(RedisScript.of(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
		try {
			TimeUnit.MILLISECONDS.sleep(50);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
	this.renewExpire();
	try {
		// 业务代码
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		String unlockScript =
				"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
						"   return nil " +
						"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
						"   return redis.call('del',KEYS[1]) " +
						"else " +
						"   return 0 " +
						"end";
		stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
	}
}

redisson实现

在这里插入图片描述

高可用 Redisson

redis分布式锁失效的情况

redis-master宕机,由于redis复制是异步的,锁信息没同步到新的master,这里新线程获取锁成功,就导致redis分布式锁失效

官网描述

在这里插入图片描述

在这里插入图片描述

Redlock算法

使用 N 个独立的master,N = 2x + 1,尝试在全部N个master上获取锁,只有能在其中至少 (N/2+1)个master上获取到锁,才算加锁成功,否则解锁全部实例

官网描述

在这里插入图片描述

watchdog

在这里插入图片描述

在这里插入图片描述

高性能 读写锁

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock writeLock = readWriteLock.writeLock();
RLock readLock = readWriteLock.readLock();
try {
    writeLock.lock();
    //业务操作
} catch (InterruptedException e) {
   log.error(e);
} finally {
    rLock.unlock();
}

通过将锁分为读锁与写锁,最大的提升之后就在与大大的提高系统的读性能,因为读锁与读锁之间是没有冲突的,不存在互斥,然后又因为业务系统中的读操作是远远多与写操作的,所以我们在提升了读锁的性能的同时,系统整体锁的性能都得到了提升

读写锁特点

  • 读锁与读锁不互斥,可共享
  • 读锁与写锁互斥
  • 写锁与写锁互斥

3、总结

所以如果我们的业务场景,更需要数据的一致性,我们可以使用 CP 的分布式锁,例子 zookeeper

如果我们更需要的是保证数据的可用性,那么我们可以使用 AP 的分布式锁,例如 Redis

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值