day104-缓存-分布式锁-分布式锁原理与使用

1.原理

分布式锁的一个基本原理就是多个服务内的多个线程访问某资源时,全都到同一个地方占坑,这个地方就是缓存,被所有服务共享的缓存,第一个访问的线程会去缓存中设置

一个key value 缓存,由于是原子性的操作,后面的线程设置同样key时缓存失败,从而实现一个分布式的锁

2.linux命令实现方式

指令参考

http://www.redis.cn/commands/set.html

从上链接可以看到当给set指令添加NX选项时,只有当key不存在时才能缓存该数据

(1)复制四个链接

(2)发送同一条命令连接到客户端(记得先切root权限)

扫描二维码关注公众号,回复: 12859052 查看本文章

(3)连接上后再次同时发送缓存命令

连接1

连接2,3,4全部是如下界面,都返回nil 也就是null

可以看到NX缓存模式下,key值相同时,只有第一次缓存会成功  这就是分布式锁的基本原理

3.代码实现方式与完善优化

初步实现,核心代码如下,就是在我们一个线程访问进行(查询数据库并保存到数据到缓存)时,给其加上分布式锁,也就是在redis中以NX模式设置一个key为lock的缓存,当其它线程访问时,若设置缓存失败也就是获取分布式锁失败,进行自旋,也就是重复的调用本方法再次进行尝试,当然为了尝试不要太频繁可设置休眠时间

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //先从缓存中尝试获取,若为空则从数据库中获取然后放入缓存
        String catalogJson;
        synchronized (this) {
            ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
            catalogJson = opsForValue.get("catalogJson");
            System.out.println("从缓存内查询...");
            if (StringUtils.isEmpty(catalogJson)) {
                Map<String, List<Catelog2Vo>> catalogJsonFromDb = null;
                try {
                    catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("从数据库内查询...");
                return catalogJsonFromDb;
            }
        }

        Map<String, List<Catelog2Vo>> catalogJsonFromCache = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return catalogJsonFromCache;
    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {

        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");

        if(lock){
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            //删除锁
            stringRedisTemplate.delete("lock");
            return dataFromDb;
        }else{
            Thread.sleep(100);
           return getCatalogJsonFromDbWithRedisLock();
        }

    }

分析下以上实现存在的问题,删除锁之前出现异常或者断电,宕机等情况时会造成死锁

,所以我们给该key(锁)设置一个过期时间

这次由于我们是分开设置的,如果获取锁之后设置过期时间之前断电啥的,也会导致死锁

解决思路:缓存与缓存数据的过期时间设置必须为原子性的,redis支持,所以改为以下代码

        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",30,TimeUnit.SECONDS);

可是还有问题,当我们查询数据库并添加到缓存或者处理业务逻辑时间过长,超过我们设置的过期时间,此时如果逻辑执行完后删除锁就是删除的别的线程的了

核心代码,值设置为uuid来方便删除时识别是否为当前线程的锁

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {

        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",30,TimeUnit.SECONDS);
        String uuid = UUID.randomUUID().toString();
        if(lock){
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            //删除锁 如果是当前线程的才删除 不然可能删的是线程的锁
            String value = stringRedisTemplate.opsForValue().get("lock");
            if(uuid.equals(value)){
                stringRedisTemplate.delete("lock");
            }
            return dataFromDb;
        }else{
            Thread.sleep(100);
           return getCatalogJsonFromDbWithRedisLock();
        }
    }

可是还有问题,由于获取value对比与删除缓存不是原子性操作,获取value之后与删除缓存之前万一宕机停电啥的,也会导致删除别人的锁,相当于没锁住

所以再次对代码进行改造,用redis提供的脚本删锁方式来保证原子性

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {

        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
        String uuid = UUID.randomUUID().toString();
        if(lock){
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            }finally {
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                //保证获取value对比与删除缓存是原子操作,这里采用redis提供的执行脚本方式
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else{
            Thread.sleep(200);
           return getCatalogJsonFromDbWithRedisLock();
        }
    }

此时就演进成了阶段五最终形态

至此就基本已经实现并完善了分布式锁,压测这里我就不做了,结果是可以预想的会成功

由于代码的重复性,可以将其封装,当然这些别人都已经做好了,后面将会了解分布式锁更专业的框架--Redisson

本篇主要参考链接:

http://www.redis.cn/commands/set.html

猜你喜欢

转载自blog.csdn.net/JavaCoder_juejue/article/details/113777382