分布式锁
在面对高并发业务时,单个项目解决不过来,此时一个项目部署到多个机器,这就是集群模式,不同的项目实例就会对应不同的端口和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(){// 获取当前线程valueString 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]) thenredis.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转换为listCollections.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 {@Beanpublic 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,超时释放时间20sboolean 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();}