目录
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁需要满足的条件:
分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:
实现分布式锁时需要实现的两个基本方法:
1.获取锁
互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败返回false
2.释放锁
手动释放
超时释放:获取锁时添加一个超时时间
流程:
代码实现:
需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能
- public class SimpleRedisLock implements ILock{
- private static final String KEY_PREFIX="lock:";
-
- @Override
- public boolean tryLock(long timeoutSec) {
- //获取线程标示
- String threadId = Thread.currentThread().getId();
- //获取锁
- Boolean success = stringRedisTemplate.opsForValue()
- .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
- return Boolean.TRUE.equals(success);
- }
-
- @Override
- public void unlock() {
- //通过del删除锁
- stringRedisTemplate.delete(KEY_PREFIX + name);
- }
- }
- @Override
- public Result seckillVoucher(Long voucherId) {
- // 1.查询优惠券
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- // 2.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- // 尚未开始
- return Result.fail("秒杀尚未开始!");
- }
- // 3.判断秒杀是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- // 尚未开始
- return Result.fail("秒杀已经结束!");
- }
- // 4.判断库存是否充足
- if (voucher.getStock() < 1) {
- // 库存不足
- return Result.fail("库存不足!");
- }
- Long userId = UserHolder.getUser().getId();
- //创建锁对象(新增代码)
- SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
- //获取锁对象
- boolean isLock = lock.tryLock(1200);
- //加锁失败
- if (!isLock) {
- return Result.fail("不允许重复下单");
- }
- try {
- //获取代理对象(事务)
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- //释放锁
- lock.unlock();
- }
- }
持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明
解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除
需求:
修改之前的分布式锁实现,满足:
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
如果一致则释放锁
如果不一致则不释放锁
代码实现:
获取锁
- private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
- @Override
- 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);
- }
- }
线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题
Lua脚本解决多条命令原子性问题
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法:Lua 教程 | 菜鸟教程
Redis提供的调用函数:
redis.call('命令名称', 'key', '其它参数', ...)
例:执行set name jack
- # 执行 set name jack
- redis.call('set', 'name', 'jack')
例:先执行set name Rose,再执行get name
- # 先执行 set name jack
- redis.call('set', 'name', 'Rose')
- # 再执行 get name
- local name = redis.call('get', 'name')
- # 返回
- return name
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
例:执行 redis.call('set', 'name', 'jack') 这个脚本
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
Lua脚本实现释放锁:
- -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
- -- 获取锁中的标示,判断是否与当前线程标示一致
- if (redis.call('GET', KEYS[1]) == ARGV[1]) then
- -- 一致,则删除锁
- return redis.call('DEL', KEYS[1])
- end
- -- 不一致,则直接返回
- return 0
利用Java代码调用Lua脚本改造分布式锁
- private static final DefaultRedisScript
UNLOCK_SCRIPT; - static {
- UNLOCK_SCRIPT = new DefaultRedisScript<>();
- UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
- UNLOCK_SCRIPT.setResultType(Long.class);
- }
-
- public void unlock() {
- // 调用lua脚本
- stringRedisTemplate.execute(
- UNLOCK_SCRIPT,
- Collections.singletonList(KEY_PREFIX + name),
- ID_PREFIX + Thread.currentThread().getId());
- }
总结
基于Redis的分布式锁实现思路:
利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性
基于setnx实现的分布式锁存在下面的问题:
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
Redission提供了分布式锁的多种多样的功能:
官网:https://redisson.org
1.引入依赖:
- <dependency>
- <groupId>org.redissongroupId>
- <artifactId>redissonartifactId>
- <version>3.13.6version>
- dependency>
2.配置Redisson客户端:
- @Configuration
- public class RedissonConfig {
-
- @Bean
- public RedissonClient redissonClient(){
- // 配置
- Config config = new Config();
- config.useSingleServer().setAddress("redis://127.0.0.1:6379");
- // 创建RedissonClient对象
- return Redisson.create(config);
- }
- }
3.使用Redission的分布式锁:
- @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();
- }
-
- }
-
- }
4.在 VoucherOrderServiceImpl注入RedissonClient:
- @Resource
- private RedissonClient redissonClient;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- // 1.查询优惠券
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- // 2.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- // 尚未开始
- return Result.fail("秒杀尚未开始!");
- }
- // 3.判断秒杀是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- // 尚未开始
- return Result.fail("秒杀已经结束!");
- }
- // 4.判断库存是否充足
- if (voucher.getStock() < 1) {
- // 库存不足
- return Result.fail("库存不足!");
- }
- Long userId = UserHolder.getUser().getId();
- //创建锁对象 使用分布式锁
- RLock lock = redissonClient.getLock("lock:order:" + userId);
- //获取锁对象
- boolean isLock = lock.tryLock();
-
- //加锁失败
- if (!isLock) {
- return Result.fail("不允许重复下单");
- }
- try {
- //获取代理对象(事务)
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- //释放锁
- lock.unlock();
- }
- }
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制
超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间
为了提高redis的可用性,会搭建集群或者主从,以主从为例
此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了
为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性
MultiLock加锁原理:
当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试
1.不可重入Redis分布式锁
2.可重入的Redis分布式锁
3.Redisson的MultiLock