基于Redis的分布式锁

Redis分布式锁

1、锁场景

  • 需要对同一共享资源进行写操作
  • 对资源的访问是互斥的

任务通过竞争获取锁才能才能对该资源进行操作(竞争锁)

当有一个任务对资源进行操作时(占有锁)

其他任务不能对该资源进行操作(任务阻塞)

直到该任务操作结束(释放锁)

竞争锁 -> 占有锁 -> 任务阻塞 -> 释放锁

graph LR
A(竞争锁) -->B(占有锁) 
B(占有锁) --> C[任务阻塞] 
C[任务阻塞] --> D[释放锁]

2、实现思路

加锁:

通过setnx向特定的key写入一个随机值,并设置失效时间,写值成功即加锁成功

1、必须给锁设置失效时间:避免死锁

2、加锁时,每个节点产生一个随机字符串:避免误删锁

3、写入随机值与设置失效时间同一命令:保证原子性

解锁:

匹配随机值,删除redis上特定的key数据,保证获取数据,判断一致性,以及删除数据也是原子性

    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end

3、代码实现

1、RedisTemplate实现

基于RedisTemplate简单封装的RedisHelper,这不是重点

  • 实现Lock接口,完成加锁解锁的redis操作
  • 1、tryLock:加锁操作(注意设置有效时间防止死锁,和操作原子性)
  • 2、Lock:加锁等待,实现可重入锁。用带失效监听的中间件自然更优雅
  • 3、很多教程解锁用的lua脚本。我感觉一句读操作一句写操作不会产生不一致的问题,就直接解锁了。
    @Component
    @Scope("prototype")
    public class RedisLock implements Lock {
    
        /**
         * 锁的key
         */
        @Setter
        private String key;
        private String value;
        @Setter
        private long seconds = 10;
    
        @Resource
        private RedisHelper redisHelper;
    
        /**
         * 阻塞锁
         */
        @Override
        public void lock() {
            while (!tryLock()) {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @Override
        public boolean tryLock() {
            String value = UUID.randomUUID().toString();
            Boolean b = redisHelper.setNx(this.key, value, this.seconds, TimeUnit.SECONDS);
            if (b) {
                this.value = value;
            }
            return b;
        }
    
        @Override
        @SneakyThrows
        public boolean tryLock(long time, @NonNull TimeUnit unit) {
            String value = UUID.randomUUID().toString();
            Boolean b = redisHelper.setNx(this.key, value, time, unit);
            if (b) {
                this.value = value;
            }
            return b;
        }
    
        @Override
        public void unlock() {
            String value = redisHelper.get(key);
            if (value.equals(this.value)) {
                redisHelper.del(key);
            }
        }
    
        @Override
        @NonNull
        public Condition newCondition() {
            return null;
        }
    }
  • 使用:
    @Resource
    private RedisLock redisLock;
    
    /**
     * 测试并发扣除库存,调用前先手动在redis中设置库存数量
     * set store 100
     */
    @GetMapping("/getStore")
    public R<Integer> getStore() {
    
        int i;
        // 阻塞加锁
        redisLock.setKey("store-lock");
        redisLock.lock();
        try {
            String store = redisHelper.get("store");
            if ("0".equals(store)) {
                return R.error("库存不足");
            }
            i = Integer.parseInt(store) - 1;
            log.info("剩余库存数量:{}", i);
            redisHelper.set("store", String.valueOf(i));
        } finally {
            // 释放锁
            redisLock.unlock();
        }
        return R.ok(i);
    }

2、Redisson实现

自动续约,不用自己开线程检查

  • 依赖
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.11.0</version>
    </dependency>
  • 配置
    /**
     * redisson配置
     */
    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
                                                        .setPassword("654321");
        return (Redisson) Redisson.create(config);
    }
  • 使用
    @Resource
    private Redisson redisson;
    
    @GetMapping("/getStore1")
    public R<Integer> getStore1() {
        String lockKey = "redisLockKey";
        RLock lock = redisson.getLock(lockKey);
        lock.lock();
        int i;
        // 阻塞加锁
        try {
            String store = redisHelper.get("store1");
            if ("0".equals(store)) {
                return R.error("库存不足");
            }
            i = Integer.parseInt(store) - 1;
            log.info("剩余库存数量:{}", i);
            redisHelper.set("store1", String.valueOf(i));
        } finally {
            // 释放锁
            lock.unlock();
        }
        return R.ok(i);
    }

3、封装功能

  • 定义注解(可重入锁注解,其他类型略)
    /**
     * @description 可重入锁注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface TryLock {
    
        /**
         * 锁的key,必须指定
         */
        String key();
    
        /**
         * 自动释放锁的超时时间:默认为10
         */
        long expire() default 10;
    
        /**
         * 时间单位:默认为秒
         */
        TimeUnit timeUnit() default TimeUnit.SECONDS;
    
    }
  • 定义注解处理器
    @Slf4j
    @Aspect
    @Component
    @AllArgsConstructor
    public class LockHandler {
    
        private Redisson redisson;
    
        /**
         * 可重入锁处理
         */
        @SneakyThrows
        @Around("@annotation(tryLock)")
        public Object lockAroundAction(ProceedingJoinPoint proceeding, TryLock tryLock) {
            String key = tryLock.key();
            long expire = tryLock.expire();
            TimeUnit timeUnit = tryLock.timeUnit();
            RLock lock = redisson.getLock(key);
            lock.lock(expire, timeUnit);
            try {
                return proceeding.proceed();
            } finally {
                lock.unlock();
            }
        }
    
    }
  • 使用
    // 注解并指定key
    @TryLock(key = "store2_lock")
    @GetMapping("/getStore2")
    public R<Integer> getStore2() {
        int i;
        // 阻塞加锁
        String store = redisHelper.get("store2");
        if ("0".equals(store)) {
            String msg = "库存不足";
            log.info(msg);
            return R.error(msg);
        }
        i = Integer.parseInt(store) - 1;
        log.info("剩余库存数量:{}", i);
        redisHelper.set("store2", String.valueOf(i));
        return R.ok(i);
    }

转载于:https://www.cnblogs.com/zuier/p/11029424.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值