【分布式应用】分布式锁学习总结

目录

应用场景:

基本原理:

实现方式一:

使用redis做分布式锁

实现方式二:

使用Redission做分布式锁


应用场景:

当前分布式应用中,往往存在这样的场景,高并发流量访问db,为了防止数据库被打挂,通常在db层以上加一层cache。

当今市面上使用的最多的缓存是用redis来作缓存,众所周知,redis缓存存在三个常见问题:

  • 缓存穿透:高并发访问的数据在数据库(db,cache)根本没有,导致流量穿过cache直接打到db
  • 缓存雪崩:大量数据同时在cache中失效,导致流量直接打到db
  • 缓存击穿:高并发流量访问热点数据,当热点数据在cache中失效的时候,导致高并发直接打到db

而应对上述问题的办法往往是:

  • 缓存穿透:设置空结果缓存,并设置过期时间,使不存在的数据也可以使用缓存
  • 缓存雪崩:设置过期时间(加随机值),使数据大概率不会同时失效
  • 缓存击穿:加锁,当热点数据在cache中失效的时候,使高并发流量中只有一个线程会进入db,并设置cache,之后流量可以访问cache获取数据

针对解决第三个问题中的加锁,

当没有加分布式锁,而是只用类似 synchronize(this) 这样的方式加本地锁的话,对于分布式应用,每个服务都会使用自己单独的锁,从而导致整个分布式应用还是会有很多线程进入db,从而可能发生不可预计的错误。

因此需要加分布式锁锁住整个分布式应用。

基本原理:

同时去一个地方“占坑”,如果占到,就执行罗炯,否则就必须等待,直到释放锁。

“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。

等待可以使用自旋的方式。

实现方式一:

使用redis做分布式锁

思路:

使用redis的SET “lock” value NX方式占坑,这里要注意几个点。

1、设置过期时间,防止setnx占好了位,业务代码异常或者redis在业务执行过程中宕机,从而导致没有执行锁删除逻辑,造成死锁。

2、设置过期时间和占位必须是原子的,redis支持使用setnx ex命令。

3、由于业务时间可能很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。因此我们需要指定锁的值为uuid,每个人匹配自己的锁才删除。

4、接续上一条,如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值,那么我们删除的就是别人的锁了。因此必须保证锁删除具有原子性,使用redis+Lua脚本完成。

//从数据库取数据 (使用redis做分布式锁)
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
        //1、占分布式锁。去redis占坑
        // 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功");
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                //加锁成功,执行业务
                dataFromDb = getDataFromDb();
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //先去redis查询下保证当前的锁是自己的
                //获取值对比,对比成功删除=原子性 lua脚本解锁
                //解锁
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...重试机制
            //休眠一百毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDBWithRedisLock();     //自旋的方式
        }
    }

实现方式二:

使用Redission做分布式锁

引入redission依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.0</version>
</dependency>

基于Redis的Redisson分布式可重入锁 RLock 对象实现了Java  concurrent包下的Lock接口。

一、配置RedissionClient

@Configuration
public class MyRedissonConfig {
    /**
     * 所有对Redisson的使用都是通过RedissonClient
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");

        //2、根据Config创建出RedissonClient实例
        //Redis url should start with redis:// or rediss://
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

二、自动注入并使用 

    @Autowired
    private RedissonClient redisson;
    @ResponseBody
    @GetMapping(value = "/hello")
    public String hello() {

        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        //2、加锁
        myLock.lock();      
        
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }

        return "hello";
    }

注意:

加锁处是阻塞式等待,默认假的锁过期时间都是30s。

Redission实现的锁,有一个“看门狗”机制,可以自动给锁续期,如果业务超长,运行时自动给锁续期,不用担心业务时间长,锁自动过期被删除。加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题。

有两种加锁方式,即是否指定过期时间

1、myLock.lock()

2、myLock.lock(10,TimeUnit.SECONDS)  //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间

对于方式一:如果我们没有指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】,只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒(internalLockLeaseTime)都会自动的再次续期,续成30秒

// internalLockLeaseTime 【看门狗时间】 / 3                      10秒

对于方式二:如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时时间就是我们制定的时间,但存在锁时间到了以后不会自动续期的问题。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值