黑马点评-实现分布式锁
1.分布式锁实现方案:
注意:使用setnx实现,在之前解决缓存击穿的时候也用了setnx这个命令实现互斥锁。
2.分布式锁实现的思路:
我们在redis中存入一个键值对,每个线程使用setnx命令看能不能设置成功,如果设置成功了则获取锁成功否则就失败线程就不会执行下面的逻辑。释放锁就是把这个键值对删除即可。
3.实现一个简单的分布式锁
简单的分布式锁工具包
public class SimpleRedisLock implements ILock {private static final String KEY_PREFIX = "lock:";private StringRedisTemplate stringRedisTemplate;private String name;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}public boolean tryLock(long timeoutSec) {// 获取线程标示long threadId = Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "",timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success);}public void unlock() {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}
这里我们把存入redis中的value设置为线程id后面有大作用。
使用分布式锁:这只是部分代码
Long userId = UserHolder.getUser().getId();
//创建锁对象(新增代码)
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {return Result.fail("不允许重复下单");
}
try {// TODO 获取代理对象(事务),这样事务才会生效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
} finally {//释放锁lock.unlock();
}
出现的问题:线程一释放了线程二的锁
原理图
4.优化分布式锁-解决线程自己释放自己设置的锁
代码实现:
工具包代码优化如下,引入UUID和线程id作为value的值,来检测是不是同一个线程
这里要添加UUID的原因是:两个jvm容易出现线程id相同的情况,所以这里不同的java程序用不同的UUID,以防止线程一删除线程二的锁
public class SimpleRedisLock implements ILock {private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";// 这里要添加UUID的原因是:两个jvm容易出现线程冲突的情况的,所以这里不同的java程序有不同的UUID标示吗,以防止线程一删除线程二的锁private StringRedisTemplate stringRedisTemplate;private String name;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId,timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}public void unlock() {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
出现的问题:因为上面的代码判断是否一致和释放锁不是原子性的操作可能期间有阻塞导致错误的释放了别的线程的锁。
原理图:
5.使用lua脚本保证操作的原子性
这里使用lua脚本保证操作的原子性,这里不讲
6.上面分布式锁还存在的问题
7.使用Redisson(官方实现好的分布式锁)
①引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>
②配置Redisson客户端
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");// 创建RedissonClient对象return Redisson.create(config);}
}
③使用Demo
@Resource
private RedissionClient redissonClient;@Test
void testRedisson() throws Exception{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);//判断获取锁成功if(isLock){try{System.out.println("执行业务"); }finally{//释放锁lock.unlock();}}
}