分布式锁

194 阅读5分钟

一:使用场景

如果是单机情况下(单JVM),线程之间共享内存,只要使用本地锁就可以解决并发问题。但如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决

二:基本概念

对于分布式应用场景下,当多个线程对公共资源访问时,需要实现排他性,传统的多线程锁已经不能满足(独立于某个进程之外),这时候就需要分布式锁的实现。 分布式锁主要是要求将锁的管理交由外部公共的第三方管理,主要有数据库、Redis、zookeeper三种 image.png 在使用分布式锁之前,我们先测试一下如果使用本地锁,在分布式场景下会出现什么问题

三:模拟分布式场景,后台启动多个服务

image.png

四:启动imeter,开启300个线程开始测试

image.png

五:查看后台日志

服务一查询了一次数据库

image.png 服务二查询了一次数据库

image.png 服务三和四也都各自查询了一次数据库

image.png 总共也就是查询了四次数据库,这说明我们的本地锁在分布式场景下失效了

六:使用分布式锁

分布式锁主要实现方式有很多种,本文初步使用redis来作为分布式锁进行测试实验 redis实现分布式锁的实现主要就是靠一个命令:SETNX key value redis官网地址:www.redis.cn/commands/se… redis官网关于该命令的详细介绍如下:

image.png 流程设计大致如下

image.png

6.1:祛除本地锁

public Map<String, List<Catelog2Vo>> getCatalogJsonLock() {
        //  synchronized (this) {}
        //得到锁以后应该再去缓存中查询一次,解决锁时序问题
        String category = redisTemplate.opsForValue().get("category");
        if (!StringUtils.isEmpty(category)) {
            Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return stringListMap;
        } else {
            //查询数据库
            return getCatalogJsonWithDb();
        }
}

6.2添加获取锁逻辑

public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
        //  synchronized (this) {}

    Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
    if(mykey){
        //得到锁以后去查询缓存
        Map<String, List<Catelog2Vo>>  map=getCatalogJsonWithRedisCatch();
        //执行业务逻辑后释放锁
        redisTemplate.delete("mykey");
        return map;
    }else{
            log.info("重试获取锁--->");
            return  getCatalogJsonWithRedisLock();
        }
    }

6.3获的锁的线程再去查询缓存

private Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisCatch() {
    //得到锁以后应该再去缓存中查询一次,解决锁时序问题
    String category = redisTemplate.opsForValue().get("category");
    if (!StringUtils.isEmpty(category)) {
        Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return stringListMap;
    } else {
        //查询数据库
        return getCatalogJsonWithDb();
    }
}

6.4开启jemter进行测试

发现四个后端服务只查询了一次数据库,说明加锁是成功的

image.png 请求中出现了一些异常,都是服务器压力过大,nginx服务崩了,先不管这个报错

image.png 仔细思考,这样简单加个setnx锁会有什么问题?

七:分布式锁的改进优化

很容易想到的第一个问题,就是setnx占位以后,在当前线程执行业务代码时,出现异常或者宕机,而没有去主动释放锁,可能就造成了死锁,其它线程就无法获得该锁

7.1设置锁的自动过期时间,即使出现异常,也会到期自动删除

改造getCatalogJsonWithRedisLock方法如下

//设置5分钟的过期时间
redisTemplate.expire("lock",300,TimeUnit.SECONDS);

7.2释放锁的时候,有可能锁已经失效(设置过期时间,突然宕机造成死锁)

解决办法:设置过期时间和占位锁必须是原子性的,对应redis中的命令是setNX EX,代码如下

public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
        //  synchronized (this) {}

    //Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
    //设置一分钟的过期时间
    Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock","1111",300, TimeUnit.SECONDS);
    if(mykey){
        //得到锁以后去查询缓存
        Map<String, List<Catelog2Vo>>  map=getCatalogJsonWithRedisCatch();
        //设置5分钟的过期时间
        //redisTemplate.expire("lock",300,TimeUnit.SECONDS);
        //执行完业务后释放锁
        System.out.println("释放锁--->");
        redisTemplate.delete("lock");

        return map;
    }else{
            System.out.println("重试获取锁--->");
            return  getCatalogJsonWithRedisLock();
        }
    }

7.3如何删除锁

假设有这样一种场景:如果由于业务时间很长,锁自己已经过期了,我们直接删除,有可能把别的线程正在持有的锁删除了 解决办法:线程占锁的时候,值指定为uuid,每个线程匹配是自己的锁才删除 改造代码如下:

public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
        //  synchronized (this) {}
    String uuId= UUID.randomUUID().toString();
    //Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
    //设置一分钟的过期时间
    Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock",uuId,300,TimeUnit.SECONDS);
    if(mykey){
        //得到锁以后去查询缓存
        Map<String, List<Catelog2Vo>>  map=getCatalogJsonWithRedisCatch();
        //redisTemplate.expire("lock",300,TimeUnit.SECONDS);
        //执行完业务后释放锁
        System.out.println("释放锁--->");
        String value=redisTemplate.opsForValue().get("lock");
        if(StringUtils.equals(uuId,value)){
            //删除自占锁
            redisTemplate.delete("lock");
        }
        return map;
    }else{
            System.out.println("重试获取锁--->");
            return  getCatalogJsonWithRedisLock();
        }
    }

7.4上述删除锁的问题

如果正好判断是当前值,正要删除锁的时候,这个时候锁过期了,别的线程已经设置到了新的值,那么就会删除别的线程正在占用的锁

解决办法:保证删除锁的原子性

redis官网有这样一段介绍 value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

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

代码如下:

public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
        //  synchronized (this) {}
    String uuId= UUID.randomUUID().toString();
    //Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
    //设置一分钟的过期时间
    Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock",uuId,300,TimeUnit.SECONDS);
    Map<String, List<Catelog2Vo>>  dataFromRedis=null;
    if(mykey){
        try{
            dataFromRedis=getCatalogJsonWithRedisCatch();
        }catch (Exception e){
        }finally {
            String lua="if redis.call("get",KEYS[1]) == ARGV[1] then\n" +
                    "    return redis.call("del",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
         redisTemplate.execute(new DefaultRedisScript<Long>(lua,Long.class), Arrays.asList("lock"),uuId);
        }
        //执行完业务后释放锁
        System.out.println("释放锁--->");
        return dataFromRedis;
    }else{
            System.out.println("重试获取锁--->");
            return  getCatalogJsonWithRedisLock();
        }
    }

7.5最后加上重试获取锁机制

System.out.println("重试获取锁--->");
try {
    TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) { e.printStackTrace(); }
return  getCatalogJsonWithRedisLock();

开启jmeter测试后满足需求,分布式锁的第一个阶段到此结束了,后续使用reddison来实现分布式锁!!!