Redis分布式锁

1、初步分布式锁

public R getListLock() {
    // 加锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    if (lock) {
        // 执行业务 ********************************************************
        // 查询缓存是否存在
        List<CategoryEntity> categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
        if (categoryEntities == null) {
            // 缓存不存在则查询数据库
            categoryEntities = baseMapper.selectList(null);
            log.info("查询数据库。。。。。。。。。。。。。");
            // 将结果缓存到redis中
            redisTemplate.opsForValue().set("tree", categoryEntities);
        } else {
            log.info("缓存命中。。。。。。。。。。。。。。。");
        }
        // 执行业务 ********************************************************
        // 释放锁
        redisTemplate.delete("lock");
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

2、优化1:加入执行业务过程中抛异常,导致无法锁无法释放,解决:使用try finally释放锁

public R getListLock() {
    // 加锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    List<CategoryEntity> categoryEntities = null;
    if (lock) {
        try{
            // 执行业务 ********************************************************
            // 查询缓存是否存在
            categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
            if (categoryEntities == null) {
                // 缓存不存在则查询数据库
                categoryEntities = baseMapper.selectList(null);
                log.info("查询数据库。。。。。。。。。。。。。");
                // 将结果缓存到redis中
                redisTemplate.opsForValue().set("tree", categoryEntities);
            } else {
                log.info("缓存命中。。。。。。。。。。。。。。。");
            }
            // 执行业务 ********************************************************
        } finally {
            // 释放锁
            redisTemplate.delete("lock");
        }
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

3、优化2:执行业务过程中服务挂掉,导致无法执行删除key释放锁,解决:给key设置过期时间

public R getListLock() {
    // 加锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    redisTemplate.expire("lock", 5, TimeUnit.SECONDS);
    List<CategoryEntity> categoryEntities = null;
    if (lock) {
        try{
            // 执行业务 ********************************************************
            // 查询缓存是否存在
            categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
            if (categoryEntities == null) {
                // 缓存不存在则查询数据库
                categoryEntities = baseMapper.selectList(null);
                log.info("查询数据库。。。。。。。。。。。。。");
                // 将结果缓存到redis中
                redisTemplate.opsForValue().set("tree", categoryEntities);
            } else {
                log.info("缓存命中。。。。。。。。。。。。。。。");
            }
            // 执行业务 ********************************************************
        } finally {
            // 释放锁
            redisTemplate.delete("lock");
        }
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

4、优化3:由于setnx命令和设置key过期命令不是原子性的,此是先set key成功上锁,在这中间如果服务挂掉会导致锁无法释放。解决:set和设置过期时间原子性执行命令

public R getListLock() {
    // 加锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1", 5, TimeUnit.SECONDS);
    List<CategoryEntity> categoryEntities = null;
    if (lock) {
        try{
            // 执行业务 ********************************************************
            // 查询缓存是否存在
            categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
            if (categoryEntities == null) {
                // 缓存不存在则查询数据库
                categoryEntities = baseMapper.selectList(null);
                log.info("查询数据库。。。。。。。。。。。。。");
                // 将结果缓存到redis中
                redisTemplate.opsForValue().set("tree", categoryEntities);
            } else {
                log.info("缓存命中。。。。。。。。。。。。。。。");
            }
            // 执行业务 ********************************************************
        } finally {
            // 释放锁
            redisTemplate.delete("lock");
        }
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

5、优化4:由于执行业务代码时候假如执行时间过长,导致redis锁过期了,那么其他线程就可以重新获取锁,并且在释放锁的时候可能会释放其他服务进程上的锁。解决:set key时加一个token标识,删除时候取出来判断是否相等再删除。

public R getListLock() {
    // 加锁
    String token = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", token, 5, TimeUnit.SECONDS);
    List<CategoryEntity> categoryEntities = null;
    if (lock) {
        try{
            // 执行业务 ********************************************************
            // 查询缓存是否存在
            categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
            if (categoryEntities == null) {
                // 缓存不存在则查询数据库
                categoryEntities = baseMapper.selectList(null);
                log.info("查询数据库。。。。。。。。。。。。。");
                // 将结果缓存到redis中
                redisTemplate.opsForValue().set("tree", categoryEntities);
            } else {
                log.info("缓存命中。。。。。。。。。。。。。。。");
            }
            // 执行业务 ********************************************************
        } finally {
            // 释放锁时先从redis取出锁判断是否相等再删除
            String ret = (String) redisTemplate.opsForValue().get("lock");
            if (token.equals(ret)) {
                redisTemplate.delete("lock");
            }
        }
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

6、优化5:上面判断token是否一致再删除虽然不会出现删错锁的问题,但是由于判断token是否一致,删除key,这两步不是原子性操作,在获取到token时候还是一致的,在进行删锁之前,由于锁过期失效,其他线程进来重新加锁,此时删锁删的就是其他线程加的锁。解决:使用redis脚本执行判断token是否一致并且删锁,保证原子性。

public R getListLock() {
    // 加锁
    String token = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", token, 5, TimeUnit.SECONDS);
    List<CategoryEntity> categoryEntities = null;
    if (lock) {
        try{
            // 执行业务 ********************************************************
            // 查询缓存是否存在
            categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
            if (categoryEntities == null) {
                // 缓存不存在则查询数据库
                categoryEntities = baseMapper.selectList(null);
                log.info("查询数据库。。。。。。。。。。。。。");
                // 将结果缓存到redis中
                redisTemplate.opsForValue().set("tree", categoryEntities);
            } else {
                log.info("缓存命中。。。。。。。。。。。。。。。");
            }
            // 执行业务 ********************************************************
        } finally {
            // 使用Lua脚本保证比较token和删除锁原子性执行
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Long ret = (Long) redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), token);
            if (ret != null && 1 == ret) {
                log.info("成功删除分布式锁。。。。。。。。。。");
            }
        }
        return R.ok().put("data", categoryEntities);
    } else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getListLock(); // 自旋重试
    }
}

7、当业务执行过长,分布式锁过期时间到了提前释放了怎么办?
可以通过检查业务是否执行完成来对锁进行续约,可以通过watchdog机制对锁进行续约,前置条件是判断redis key仍然存在,则代表业务还未执行完毕。
更推荐方案:使用redisson,reddsson对以上问题都有考虑了,并且做了实现封装。redission底层对锁续约实现是通过netty的HashedWheelTimer实现的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值