谷粒商城—缓存—分布式锁(157~166)


二.分布式锁 -前言:
      1.分布式锁 原理 与 使用: 
                 1)分布式锁 原理:
                                  a:我们可以 让 多个服务,同时去一个地方 “占锁”,如果占到,就执行逻辑,否则就必须等待,直到 释放锁。
                                  b:“占锁”,可以去 redis ,可以去 数据库,要去到 大家都能访问到的 地方,等待可以 自旋 的方式。

2.分布式锁 演进 :使用 redis 存储,占锁:

1):问题:
                                 a:setnx 占好了位置,代码异常 / 程序在页面过程中 宕机。没有执行删除锁逻辑,这就造成了死锁。
                 2):解决:
                                 a:设置 锁 的 自动过期,即使没有删除,会自动删除。
                                 b:注意:redis 加锁保证原子性,解锁保证原子性。
                                 c:如果 业务为执行完毕,锁过期,就要 锁自动延期。
                 3):代码逻辑:

@Override
public List<CategoryVo> listWithTree() {

    //占用 分布式锁,去 redis 占坑,没有key 的时候,才能设置上。存在key,就需要等待。
    //避免 删除 别人的锁
    String s = UUID.randomUUID().toString();

    //给锁设置 过期时间 30 毫秒
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);

    if (lock) {
        
        //占到锁,执行 业务,
        ...
        //查询玩之后要解锁。(删除 自己的锁)
        String lock2 = (String) redisTemplate.opsForValue().get("lock");
        if (uuid.equals(lock2)) {
            Boolean lock1 = redisTemplate.delete("lock");
        }
        
        return getDataFromDb();
    } else {
        //没占到锁,休眠 100s 后重试
        // 自旋 的方式,自己调用 自己的方法。
        return listWithTree();
    }
}

二.分布式锁 - Redisson 框架
官网地址:(https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95)

1.分布式锁 原理 与 使用:
                 1)引入 依赖:

    <!--以后 使用 redisson ,作为所有 分布式锁,分布式对象 等功能框架-->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.12.5</version>
    </dependency>

2):redisson 配置方法:(单Redis节点模式)(文档)

@Configuration
public class MyRedissonConfig {

@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {

    Config config = new Config();
    config.useSingleServer().setAddress("redis://114.215.173.88:6379");

    RedissonClient redisson = Redisson.create(config);
    return redisson;
}

}

2):redisson 简单使用方法:(文档)

@Autowired
private RedissonClient redissonClient;

@RequestMapping("/hello")
public R hello() {
    RLock lock = redissonClient.getLock("my-lock");
    //加锁 1  //阻塞式等待:拿不到锁一直等待。
    // 1).锁的 自动续期,如果业务超长,运行期间,会自动给锁续上新的 30s。不用担心业务时间长,锁自动过期被删掉。
    // 2).加锁的业务,只要运行完成,就不会给当前锁续期,即使不手动解锁,锁 默认在 30s 以后,自动删除。

// lock.lock();

    //加锁 2  // 10秒,自动落锁,自动解锁时间,一定要大于业务执行时间。

// lock.lock(20, TimeUnit.SECONDS);
//问题:10秒 锁的时间到期以后,不会自动续期
// 1).如果我们传递了 锁的超时时间,就发送给 redis 执行脚本,进行占锁,默认超时就是我们指定的时间。
// 2).如果我们 未指定锁的超时时间,就使用 30 * 1000【 看门狗 默认时间 】 ;
// 只要 占锁成功,就会启动一个定时任务【 重新给锁 设置过期时间,新的过期时间,就是看门狗的默认时间 】
// 三分之一的 看门狗时间,自动续期 。每隔十秒就是 自动再次续期,续成 30 s。

    // 最佳实战
    // 1).省掉了 这个那个续期操作,手动解锁
    lock.lock(30, TimeUnit.SECONDS);
    
    try {
        //业务代码
        System.out.println("业务代码");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        //解锁:为了防止 业务代码异常
        lock.unlock();
    }
    return R.ok();
}

2.读写锁原理 与 使用:
                 1)读写锁 作用:
                                  a:改数据加写锁,读数据加读锁。( 改的时候不能读,读的时候不能改 )(写锁是一个 互斥锁,独享锁)
                                  b:写锁没释放,读必须等待
                                  c:几种情况分析:
                                          先写 + 后读 :等待写锁释放
                                          先写 + 后写 :等待写锁释放
                                          先读 + 后写 :等待 读锁释放,才能进行写锁加锁。
                                          先读 + 后读 :不需要等待 读锁的 释放,相当于 无锁状态,所有的读锁,都会加锁成功。
                                  d:总结:只要有写的存在,都要 等待锁。
                 2)代码示例  –  写锁:

@RequestMapping("/write")
public R write() throws InterruptedException {

    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-clock");
    RLock rLock = readWriteLock.writeLock();

    String s = "";
    try {
        //1)改数据,加写锁
        rLock.lock();

        s = UUID.randomUUID().toString();
        Thread.sleep(30000);
        stringRedisTemplate.opsForValue().set("write", s);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return R.ok().put("data", s);
}

3)代码示例  –  读锁:

@RequestMapping("/read")
public R read() throws InterruptedException {
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-clock");
    RLock rLock = readWriteLock.readLock();
    rLock.lock();

    String read = null;
    try {
        read = stringRedisTemplate.opsForValue().get("write");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return R.ok().put("data", read);
}

3.闭锁 原理 与 使用:
                 1)闭锁 介绍:
                                  a:等待 其他 全部完成,才来执行主程序。
                                  b:案例:学校放假锁门,要等 5 个班,全部没人了,才能锁门。
                                        解决:上面 主程序,下面 子程序,(子程序都走完,主程序才走)

@RequestMapping("/lockDoor")
public R lockDoor() throws InterruptedException {

    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    //等待 闭锁都完成(等待,计数 自减 5次)
    door.trySetCount(5);
    door.await();

    return R.ok();
}

public String gogogo(Long id) {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    //计数 减一
    door.countDown();
    return id + "班级人都走了";
}

4.信号量 原理 与 使用:
                 1)信号量 介绍:
                                  a:
                                  b:案例 1:车库停车,一共三个车位,来一个车加一个;走一个车,减掉一个。
                                        解决:上面程序是车进来,下面程序是车走。

@RequestMapping("/parkcome")
public R parkcome() throws InterruptedException {

    RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
    //获取一个信号(一个值)(占一个车位)
    //阻塞式获取,一定要获取到一个车位。
    semaphore.acquire();
    return R.ok();
}

@RequestMapping("/parkgo")
public R parkgo() throws InterruptedException {

    RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
    //释放一个信号(一个值)(释放一个车位)
    semaphore.release();
    return R.ok();
}

c:案例 1:限流操作:
                                       解决:如果 没有获取到车位,直接返回 false。不阻塞。

@RequestMapping("/parkcome")
public R parkcome() throws InterruptedException {

    RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
    //尝试 获取一个车位。不阻塞,没有直接返回 false
    boolean b = semaphore.tryAcquire();
    if (b) {
        //执行业务
    } else {
        return R.ok().put("data", "当前人数过多,请稍后访问");
    }
    return R.ok();
}

4.分布式 缓存一致性 解决:
                 1)缓存数据 和 数据库数据,保持一致:
                                  a:双写模式:改完数据库,更新缓存。
                                       
                                       双写模式 会产生 缓存数据不一致解决方案:
                                                          方案1:写 数据库 和 写缓存,加锁,保持 数据库 和 缓存 数据一致性。
                                                          方案2:看 服务 允不允许 暂时性 数据不一致 问题。缓存到了过期时间后,自动删除。缓存更新。
                                  b:失效模式:改完数据库,删除缓存。(使用多)
                                      
                                       失效模式 会产生 缓存数据不一致解决方案:(和双写模式解决方案一样)
                                                          方案1:写 数据库 和 写缓存,加锁,保持 数据库 和 缓存 数据一致性。
                                                          方案2:看 服务 允不允许 暂时性 数据不一致 问题。缓存到了过期时间后,自动删除。缓存更新。

2)缓存数据 一致性 解决方案:
                                  a:无论是 双写模式 还是 失效模式,都会导致 缓存的不一致问题,即 多个实例同时更新 会产生。
                                                          情况 1:如果是 用户 纬度数据,这种并发几率很小,不用考虑这个问题,缓存数据加上过期时间,一段时间自动更新即可。                                                                                                                   情况 2:如果是 菜单,商品介绍等基础数据,大部分是可以容忍暂时缓存不一致问题。(也可以使用 canal 订阅 binlog 的方式 解决)
                                                          情况 3:缓存数据 + 过期时间,也足够解决 大部分业务对于缓存的要求。
                                                          情况 4:加 读写锁,写的时候排队,读不需要排队。
                                  b:我们系统 一致性解决方案:
                                                          方案 1:缓存所有的数据,都有过期时间,数据过期,下一次查询主动触发更新。
                                                          方案 2:读写数据的时候,加上 分布式的 分布式读写锁。经常写,经常读。
                                  c:使用 Canal :只要更新 数据库,就自动更新缓存。

3)总结:
                                  a:放入缓存的,就不应该是,实时性,一致性,要求超高的,所以,大部分缓存,只要加上过期时间,保证拿到当天最新数据即可。
                                  b:遇到实时性,一致性要求高的数据,就应该直接查询数据库,即使速度会慢一点,

上一篇:[LeetCode] 157. Read N Characters Given Read4


下一篇:网络流——最大流-Dinic算法