黑马程序员redis入门到实战(分布式锁含redisson)
1.分布式锁的出现
当我们处于集群分布式,原先加的用户锁,就不能锁住单体用户,如果在不同的jvm中同一用户再次抢卷,仍可能出现一人抢多张卷的情况,如图:

所以需要一个锁监视器来监视全部的锁,下面时分布式锁的工作原理:

2.什么是分布式锁
分布式锁:满足分布式系统或者集群模式下多线程可见并且互斥的锁
一般满足一下五项必须特性
- 多线程可见
- 互斥
- 高可用
- 高性能
- 安全性
还有些功能性特征,比如:是否满足可重入性,获取锁是阻塞的还是非阻塞的等等
3.分布式锁的实现
分布式锁的核心是多线程之间的互斥,而满足这一点的方法有很多,常见有三种:

基于redis实现分布式锁
基于redis的分布式锁的实现原理

3.1实现redis分布式锁版本1
package com.hmdp.utils;
public interface ILock {
/**
尝试获取锁
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unLock();
}
package com.hmdp.utils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class SimpleRedisLock implements ILock{
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final String KEY_PREFIX = "lock:";//锁的key的统一前缀
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程的标识
long id = Thread.currentThread().getId();
//获取锁
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, String.valueOf(id), timeoutSec, TimeUnit.SECONDS));
}
@Override
public void unLock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
//加悲观锁
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
//获取锁 ,正常不需要设置这么长时间,这里是为了方便调试
boolean b = redisLock.tryLock(12000);
//判断获取锁成功
if (!b) {
//获取锁失败,返回失败
return Result.fail("请勿重复下单");
}
try {
//拿到当前对象的代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//7.返回订单ID 利用代理对象调用createVoucherOrder方法,事务才能正常回滚
return proxy.createVoucherOrder(id);
} finally {
//释放锁
redisLock.unLock();
}
3.2分布式锁误删问题
当线程1业务阻塞时,线程1的锁因超时自动释放了,这时线程2进来获取锁,获取锁后,线程1业务完成释放锁,直接把线程2的锁给释放了,如果这时来了个线程3获取锁也能获取成功,这是就会出现两把锁,产生线程安全问题,发生了分布式锁的误删,这就是实现redis分布式锁版本1的缺点,过程如图:

由于这个原因,我们需要给锁加一个标识,保证每一个线程释放的锁是自己创建的锁,如图:

分布式锁的改进方案,如图:

只需要修改锁的实现类即可
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final String KEY_PREFIX = "lock:";//锁的key的统一前缀
private final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";//利用uuid生成线程标识,参数true表示删除uuid中的-
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程的标识
String id = ID_PREFIX + Thread.currentThread().getId();
//获取锁
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id, timeoutSec, TimeUnit.SECONDS));
}
@Override
public void unLock() {
//获取线程标识
String id = ID_PREFIX + Thread.currentThread().getId();
//判断标识是否一致
String redisId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (id.equals(redisId)) {
//一致,释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
3.3分布式锁的原子性问题
当业务在判断锁是否唯一后,发生了阻塞 (阻塞原因是jvm垃圾回收) 虽然判断动作结束后就是释放锁,但是中间仍可能因为垃圾回收引起线程阻塞,导致超市释放锁,又有一个线程2在阻塞过程中,超时释放锁结束后获取锁,然后线程1把线程2的锁释放了,又再次发生线程安全问题,出现这种情况的原因,就是锁标识和释放锁是两个动作,不能保证判断锁和释放锁成为一个原子性的动作。

redis的事务只能保证原子性,但是不能保证一致性,可以利用redis事务结合乐观锁的忘却机制实现,但是很复杂
在redis中提供Lua脚本功能,在一个脚本中编写多条redis命令,确保原子性。Lua是一种编程语言,基本语法参考网站https://www.runoob.com/lua/lua-basic-syntax.html
在resources资源目录下创建unlock.lua资源文件
--这里的KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标识
--获取锁中的标识,判断是否与当前线程标识一致
if(redis.call('GET',KEYS[1]==ARGV[1])) then
--一致,则删除锁
return redis.call('DEL',KEYS[1])
end
--不一致,则直接返回
return 0
由上述分析可知,出现问题的原因只和释放锁有关,所以我们是改动释放锁的方法
//加载lua脚本静态资源
private static final DefaultRedisScript< Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
//不再是自己释放锁,而是通过lua脚本释放锁
@Override
public void unLock() {
//调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
4.redisson
上述redis分布式锁可以解决大多数问题,但是基于setnx实现的分布式锁仍存在下面情况:

于是就出现了redisson,redisson是一个在redis基础上实现java驻内存数据网络。他不仅提供了一系列分布式的java常用对象,还提供了许多分布式服务,其中包含各种锁的实现:

4.1redisson的快速入门
引入xml依赖
<!-- redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.2</version>
</dependency>
redisson也可以在yaml文件中配置,但是配置会覆盖原来的redis配置,所以这里单独配置
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig {
@Bean
public RedissonClient redisClient() {
//配置
Config config=new Config();
String address = "redis://" + host + ":" + port;
config.useSingleServer()
.setAddress(address)
.setPassword(password)
.setDatabase(database);
//创建redisson对象
return Redisson.create(config);
}
}
其实这里也可以读取yaml文件中的redis配置属性,这样也能做到更好的解耦,
属性多的话,可以在类上加注解@ConfigurationProperties(prefix = “spring.redis”)少的话可以在属性上加注解,如@Value(“${spring.redis.host}”),同时记得需要在启动类或配置类上添加 @EnableConfigurationProperties 来启用配置属性功能。
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private String host;
private int port;
private String password;
private int database;
// getters and setters
}
使用redisson分布式锁

4.2可重入锁的原理
将原来redis存的key:String变成key:hash类型,保证一次能存入线程标识和获取锁的次数,每次获取锁,hash.value就+1(同时重置有效期),每次释放锁就-1,当=0时就删除锁,整体过程,如图:

原理的过程很多,为了保证事务的原子性,肯定不能在依靠java代码了,我们需要借用lua脚本实现,如图:
- 获取锁

- 释放锁

redisson分布式锁的原理
watchDog看门狗机制,只要转入参数leaseTime=-1时才会执行,这是一个重置锁过期时间的方法,自动续约方案


4.3redisson解决主从一致性导致锁失效问题
一般redis都是以集群的方式部署,主节点负责写修改功能,从节点负责读的功能,主节点修改后,会向从节点进行同步(中间虽然很快,但如果主节点修改完,还没向从节点同步就宕机的话,会从从节点服务中重新挑选一个主节点,重选后却不带原来的锁,就会出现主从一致导致锁丢失问题),效果图如图:

在redisson中,我们将全部的从节点都变成主节点,同时从所有节点中获取锁,如果有一个获取失败了,就不能获取锁,同时确保安全,我们也可以在全部节点后都添加从节点,这样就算其中一个主节点失效了,也可以从节点顶替成主节点,就算顶替的从节点中没有锁,我们需要全部都获取锁才能成功获取锁,所有也对整个业务没有影响,只是让java应用重新获取锁而已。

这种行为在redisson中叫做连锁。
5.总结

更多推荐


所有评论(0)