深入浅出分布式锁

参考

深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

尚硅谷2022最新版分布式锁系列视频

快速上手

三种实现

redis、zookeeper、数据库(MySQL)

JMeter

深入浅出Apache JMeter

redis

本地Redis中模拟库存扣减的过程。
在这里插入图片描述

版本一

    @GetMapping("sell")
    public String sell() {
        int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
        if (sell > 0) {
            int realStock = sell - 1;
            stringRedisTemplate.opsForValue().set("sell", realStock + "");
            System.out.println(port + ":扣除成功,剩余库存" + realStock);
        } else {
            System.out.println(port + ":扣减失败,库存不足");
        }
        return port + "end";
    }

在这里插入图片描述
出现了重复扣减,我们进行改进。

版本二

    @GetMapping("sell2")
    public String sell2() {
        synchronized (this) {
            int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
            if (sell > 0) {
                int realStock = sell - 1;
                stringRedisTemplate.opsForValue().set("sell", realStock + "");
                System.out.println(port + ":扣除成功,剩余库存" + realStock);
            } else {
                System.out.println(port + ":扣减失败,库存不足");
            }
            return port + "end";
        }
    }

在这里插入图片描述
synchronized 无法跨jvm进行锁定,当有多台机器时,需要使用分布式锁来保证其正确性。

版本三

使用 Redis 的 setIfAbsent
在这里插入图片描述

public String sell3() {
        String lock = "lock";
        //加锁
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, "this is lock");
        if (result) {
            int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
            if (sell > 0) {
                int realStock = sell - 1;
                stringRedisTemplate.opsForValue().set("sell", realStock + "");
                System.out.println(port + ":扣除成功,剩余库存" + realStock);
            } else {
                System.out.println(port + ":扣减失败,库存不足");
            }
            //删除锁
            stringRedisTemplate.delete(lock);
            return port + "end";
        }
        return "获取锁错误";
    }

加锁部分如果出现异常会导致锁无法释放。

版本四

使用 try catch来保证程序内异常仍然可以释放锁。

    public String sell4() {
        String lock = "lock";
        try {
            boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, "this is lock");
            if (result) {
                int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
                if (sell > 0) {
                    int realStock = sell - 1;
                    stringRedisTemplate.opsForValue().set("sell", realStock + "");
                    System.out.println(port + ":扣除成功,剩余库存" + realStock);
                } else {
                    System.out.println(port + ":扣减失败,库存不足");
                }
            } else {
                return port + "获取锁错误";
            }
        } finally {
            stringRedisTemplate.delete(lock);
        }
        return port + "end";
    }

虽然程序内异常可以屏蔽,但是如果强制结束进程仍会造成死锁。

版本五 增加过期时间

注意事项:加锁的动作应该是原子性操作,如果分为两步,在第一步结束后宕机仍然存在死锁的问题。

 boolean result = stringRedisTemplate.opsForValue()
 .setIfAbsent(lock, "this is lock", 30, TimeUnit.SECONDS);

假设过期时间为10s,业务执行10.1s,存在误删除别人锁的情况。

版本六 增加唯一标识

通过 uuid 来确保删除前判断是否自己持有锁。

    @GetMapping("sell6")
    public String sell6() {
        String lock = "lock";
        String uuid = UUID.randomUUID().toString();
        try {
            boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
            if (result) {
                int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
                if (sell > 0) {
                    int realStock = sell - 1;
                    stringRedisTemplate.opsForValue().set("sell", realStock + "");
                    System.out.println(port + ":扣除成功,剩余库存" + realStock);
                } else {
                    System.out.println(port + ":扣减失败,库存不足");
                }
            } else {
                return port + "获取锁错误";
            }
        } finally {
            if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))) {
                //
                //
                stringRedisTemplate.delete(lock);
            }
        }
        return port + "end";
    }

由于删除锁不是原子性,所以删除锁错误代码如下所示

//线程一
if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))) {
//锁到期了
//线程二
stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, imeUnit.SECONDS);
//线程一
stringRedisTemplate.delete(lock);
}

版本七 使用lua脚本释放锁 (原子性)

查询key对应的值,如果和uuid相等删除key,否则结束操作。

del.lua

local key = KEYS[1]
local val = redis.call('GET',key)
if val == ARGV[1]
then
redis.call('DEL', KEYS[1])
return 1
else
return 0
end

RedisScriptConfig.java

@Configuration
public class RedisScriptConfig {

    @Bean
    public DefaultRedisScript<Boolean> redisScript() {
        DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
        ClassPathResource classPathResource = new ClassPathResource("lua/del.lua");
        ResourceScriptSource resourceScriptSource = new ResourceScriptSource(classPathResource);
        script.setResultType(Boolean.class);
        script.setScriptSource(resourceScriptSource);
        return script;
    }
}
    @GetMapping("sell7")
    public String sell7() {
        String lock = "lock";
        String uuid = UUID.randomUUID().toString();
        try {
            boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
            if (result) {
                int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
                if (sell > 0) {
                    int realStock = sell - 1;
                    stringRedisTemplate.opsForValue().set("sell", realStock + "");
                    System.out.println(port + ":扣除成功,剩余库存" + realStock);
                } else {
                    System.out.println(port + ":扣减失败,库存不足");
                }
            } else {
                return port + "获取锁错误";
            }
        } finally {
            List<String> keys = Collections.singletonList(lock);
            stringRedisTemplate.execute(redisScript, keys, uuid);
        }
        return port + "end";
    }

仔细思考一下,锁的过期时间要充分冗余,避免提前过期的风险。

版本八 redisson

pom依赖

 <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.8</version>
        </dependency>

RedissionConfig

@Configuration
public class RedissionConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        if (ObjectUtils.isEmpty(password)){
            config.useSingleServer().setAddress("redis://" + host + ":" + port);
        }
        else{
            config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        }
        return Redisson.create(config);
    }
}

controller

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("sell9")
    public String sell9() {
        //获取锁
        RLock rLock = redissonClient.getLock("lock");
        try {
            //加锁
            rLock.lock();
            int sell = Integer.parseInt(stringRedisTemplate.opsForValue().get("sell"));
            if (sell > 0) {
                int realStock = sell - 1;
                stringRedisTemplate.opsForValue().set("sell", realStock + "");
                System.out.println(port + ":扣除成功,剩余库存" + realStock);
            } else {
                System.out.println(port + ":扣减失败,库存不足");
            }

        } finally {
            //解锁
            rLock.unlock();
        }
        return port + "end";
    }

版本九 自旋

redisson同样为我们提供了自旋机制

        RLock rLock = redissonClient.getSpinLock(lock);

自旋和非自旋的区别

redis作为分布式锁的主要问题

过期时间、原子性、唯一标识、异常等问题。

redis集群存在的问题

当 redis 处于集群的时候会存在各种各样的极端情况,对于资源绝对正确的业务,仍需要采取其他手段来保证数据正确,也就是不能认为redis做分布式锁是一定可靠的。

Zookeeper (一致性)

Spring Boot使用Zk实现分布式锁

引入 Apache Curator

Apache Curator 是分布式协调服务Apache ZooKeeper的 Java/JVM 客户端库。它包括一个高级 API 框架和实用程序,使 Apache ZooKeeper 的使用更加轻松和可靠

Apache Curator

        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.1</version>
        </dependency>

新建配置类负责连接Zk

@Configuration
public class ZkCuratorRecipesConfig {

    @Bean
    public CuratorFramework getZkClient() throws InterruptedException {
        String zkAddr = "192.168.17.128:2181";
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(zkAddr, retryPolicy);
        client.start();
        return client;
    }
}
    @Autowired
    CuratorFramework client;

    @GetMapping("b")
    public void b() throws Exception {
        InterProcessMutex lock = new InterProcessMutex(client, "/lock");
        if (lock.acquire(1, TimeUnit.SECONDS)) {
            try {
                logger.info("{} , {} ", Thread.currentThread().getName(), "获取到锁");
            } finally {
                lock.release();
            }
        }
    }

在这里插入图片描述
稍微改造一下接口进行压测

@GetMapping("c")
    public void c() throws Exception {
        InterProcessMutex lock = new InterProcessMutex(client, "/lock");
        String tName = Thread.currentThread().getName();
        if (lock.acquire(1, TimeUnit.SECONDS)) {
            try {
                logger.info("{} , {} ", tName, "获取到锁");
                Thread.sleep(500);
            } finally {
                lock.release();
                logger.info("{} , {} ", tName, "释放锁");
            }
        }
    }

效果图如下
在这里插入图片描述

原理分析

在这里插入图片描述

长时间没有收到心跳,zk会删除临时节点。

zk绝对安全吗

在这里插入图片描述
可以看到极端情况下,zk也有极小概率冲突。

数据库

以 MySQL为例

基于 MySQL 唯一索引的分布式锁

insert 和 delete

建一张分布式锁表,通过写入和删除数据来实现分布式锁。

悲观锁

利用for update语句。适用场景:写多读少。存在锁表风险。

乐观锁

利用 version 版本号。适用场景:读多写少。

选型和总结

侧重性能和简单使用Redis。
侧重安全性使用zookeeper。
记住极端情况互斥性都会被打破,不要认为绝对可靠。
数据库来做分布式锁会有性能瓶颈。

高并发下分布式锁优化

优化思想:锁分段、读写锁、无锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值