分布式锁
在面对高并发业务时,单个项目解决不过来,此时一个项目部署到多个机器,这就是集群模式,不同的项目实例就会对应不同的端口和JVM。
1.模拟集群模式
Nginx实现负载均衡(轮询)
2.使用集群模式下的问题
使用集群模式,不同的项目则对应不同的JVM,而锁监视器是基于JVM的,这是就会存在线程安全问题,代码相同,申请的却不是同一把锁,一把是JVM1的,另一把是JVM2的。
在解决一人抢购多个商品业务(限购)时,出现业务数据异常:
同一个人连续发送两次请求,一个到了8081、另一个在8082,都能获取到锁,查询得到的也是同一个结果,导致两个不同实例线程都能购买成功。
3.分布式锁
造成这个的原因是锁不是共享的,且不可视,JVM1看不到JVM2的锁。
解决:使他们申请的是同一把锁,实现多线程之间互斥
使用分布式锁,使不同的JVM统一锁监视器
4.使用redis实现分布式锁
使用逻辑和Lock基本一致,但是获取锁失败后不会等待而是返回false,且添加了超时释放
public boolean tryLock(String key){
Boolean flat = redisTemplate.opsForValue().setIfAbsent(key, "1", TTL, TimeUnit.SECONDS);
return flat;
}
public void unLock(String key){
redisTemplate.delete(key);
}
问题
1.锁被误删
存在线程1 tryLock()成功后,执行业务逻辑,但是发生了业务阻塞,就卡在那了,导致没有手动的 unLock(),而是因为超时而释放了,
此时线程2进来了 ,tryLock()成功后,执行业务逻辑,线程1突然好了,就去释放锁了,但是此时的锁不是线程1的,而是线程2的,线程2执行完后很懵逼,家怎么被偷了,我该释放谁啊?
解决:释放锁时增加判断,锁是自己的吗
public boolean tryLock(Long expireTime){
// 获取当前线程id
// 在集群中,线程id由所在jvm管理,所以线程id会重复
long id = Thread.currentThread().getId();
String value = uuid+id;
// 设置锁
Boolean succeed = stringRedisTemplate.opsForValue().setIfAbsent(
prefix + name, value, expireTime, TimeUnit.SECONDS);
// 拆箱
return Boolean.TRUE.equals(succeed);
}
public void unLock(){
// 获取当前线程value
String value = stringRedisTemplate.opsForValue().get(prefix + name);
// 判断是否是自己的锁
long id = Thread.currentThread().getId();
String myValue = uuid+id;
if (value.equals(myValue)){
stringRedisTemplate.delete(prefix + name);
}
}
这就行了吗,会不会有更加极端的情况,卡在了stringRedisTemplate.delete(prefix + name),判断锁逻辑已经成功,要删除时,业务阻塞了,又发生上面的锁被误删情况。
原因是这个操作不是原子性的,所以存在中途发生问题,只需要把操作写到一个脚本
2.Lua脚本
使用redis命令调用Lua脚本Lua 教程 | 菜鸟教程
基本全局变量a=10,局部变量local b=10,方法function,调用redis redis.call()
1 | EVAL script numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。 |
2 | EVALSHA sha1 numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。 |
3 | SCRIPT EXISTS script [script ...] 查看指定的脚本是否已经被保存在缓存当中。 |
4 | SCRIPT FLUSH 从脚本缓存中移除所有脚本。 |
5 | SCRIPT KILL 杀死当前正在运行的 Lua 脚本。 |
6 | SCRIPT LOAD script 将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。 |
主要看1,EVAL script(脚本)numkeys(key参数数量)key[](key参数),arg[](其他参数)
写Lua脚本
IDEA下载EmmyLua插件,创建Lua脚本在resource路径下
if (redis.call('exists', KEYS[1]) == ARGV[1]) then
redis.call('del', KEYS[1])
end
return 0
调用Lua脚本
//定义释放锁的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);
}
public void unLock(){
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
//使用Collections.singletonList(),将key转换为list
Collections.singletonList(prefix + name),
uuid+Thread.currentThread().getId()
);
}
5.Redisson
1.添加maven依赖
<!--Redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.2</version>
</dependency>
2.配置Redisson客户端
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
// 创建配置
Config config = new Config();
// 添加节点信息, ip:port和密码
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456");
return Redisson.create(config);
}
}
3.修改业务
//使用redisson获取锁对象,可重入
RLock lock = redissonClient.getLock("order:" + id);
//尝试获取锁,尝试获取锁的时间最大等待时间为1s,超时释放时间20s
boolean isLock = lock.tryLock(1,20L, TimeUnit.SECONDS);
if (!isLock){
return Result.fail("不允许重复下单");
}
try {
//手动创建代理对象
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}