21.缓存-分布式锁

该博客探讨了分布式锁的实现,包括基于Redis的简单实现和Redisson客户端的使用,如RLock和ReadWriteLock。同时,文章讨论了缓存一致性问题,如双写模式和失效模式的优缺点,并提出了解决方案,建议使用失效模式配合读写锁来确保数据一致性。
摘要由CSDN通过智能技术生成

1 基本原理

image-20220512183152242

2 实现方法

image-20220512183724114

image-20220512184411413

image-20220512184832125

image-20220512185511702

image-20220512185756621

2.1 实现代码

修改后的三个方法:

getCatalogJson

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    // 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型

    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");

    if (!StringUtils.hasText(catalogJSON)) {
        // 缓存中没有,查询数据库
        System.out.println("缓存不命中,查询数据库...");
        return getCatalogJsonFromDBWithRedisLock();
    }

    System.out.println("缓存命中,直接返回");

    // 将缓存中查询出的json转换为指定的对象
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return result;
}

getCatalogJsonFromDBWithRedisLock

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
    // 1. 占分布式锁
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
    if (lock) {
        // 加锁成功,执行业务
        System.out.println("获取分布式锁成功!!!");
        Map<String, List<Catelog2Vo>> catalogJsonFromDB;
        try {
            catalogJsonFromDB = getCatalogJsonFromDB();
        } finally {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 删除锁
            Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                    Arrays.asList("lock"), uuid);
        }
        return catalogJsonFromDB;
    }else {
        // 加锁失败,重试
        // TODO 休眠100s重试
        System.out.println("获取分布式锁失败,等待重试...");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogJsonFromDBWithRedisLock(); // 自旋获取锁
    }

}

getCatalogJsonFromDB

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {

    // 得到锁以后,在去缓存中确定一次
    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
    if (!StringUtils.isEmpty(catalogJSON)) {
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

    System.out.println("查询了数据库");

    // 查询出表pms_category所有的记录实体
    List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

    // 查出所有的一次分类
    List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);

    // 封装数据,构造一个以1级id为键,2级分类列表为值的map
    Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
        // 根据一级分类id查找二级分类
        List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());

        // 封装结果为Catelog2Vo的集合
        List<Catelog2Vo> catelog2Vos = null;

        if (level_2_categorys != null) {

            // 把 level_2_categorys 封装为 catelog2Vos
            catelog2Vos = level_2_categorys.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                // 根据二级分类id查找三级分类
                List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());

                // 将 level_3_categorys 封装为 catelog3Vos
                if (level_3_categorys != null) {
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        return catelog3Vo;
                    }).collect(Collectors.toList());

                    catelog2Vo.setCatalog3List(catelog3Vos);
                }

                return catelog2Vo;

            }).collect(Collectors.toList());

        }

        return catelog2Vos;
    }));

    // 将查到的数据放入缓存
    String s = JSON.toJSONString(collect);
    stringRedisTemplate.opsForValue().set("catalogJSON", s);
    return collect;

}

3 测试分布式锁

1、复制几个product服务配置,设置不同的端口。记得启动gateway服务。

image-20220512210900611

image-20220512213118227

2、配置测试http

image-20220512213106245

3、JMeter测试,查看创建的几个product服务的打印信息中是否只有一次==“查询了数据库”==

4 Redisson

**文档地址:**https://github.com/redisson/redisson/wiki/Table-of-Content

4.1 实践

4.1.1 导入依赖

我的SpringBoot版本是2.6.6,导入3.12.0版本的redisson有冲突,导入依赖失败,于是选择了3.16.4版本的

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

4.1.2 配置

**配置方法文档:**https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

**第三方框架整合文档:**https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88

创建MyRedissonConfig配置类,路径:com/atguigu/gulimall/product/config/MyRedissonConfig.java

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRedissonConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        // 1. 创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");

        // 2. 根据Config创建出RedissonClient
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

4.1.3 测试

@Autowired
RedissonClient redissonClient;

@Test
void redisson() {
    System.out.println(redissonClient);
}

4.2 lock锁测试

**分布式锁文档:**https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

1、修改hello方法,路径:com/atguigu/gulimall/product/web/IndexController.java

@ResponseBody
@GetMapping("/hello")
public String hello() {
    // 1. 获取一把锁,只要锁的名字一样,就是同一把锁
    RLock lock = redissonClient.getLock("myLock");

    // 2. 上锁
    lock.lock(); // 会自动续期
    // lock.lock(10, TimeUnit.SECONDS); // 10秒后解锁,不自动续期
    try {
        System.out.println("上锁成功,执行业务....当前线程号:" + Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (Exception e) {

    } finally {
        // 3. 解锁
        System.out.println("解锁...当前线程号:" + Thread.currentThread().getId());
        lock.unlock();
    }
    
    return "hello";
}

lock.lock(); 是阻塞式等待,默认锁的过期时间(TTL)为30秒。

锁可以自动续租,原理就是维持了一个定时任务,给隔10 秒把锁的过期时间设置为30 秒。如果这时候 redison 客户端退出,这个续期的定时任务被释放,锁就会过期。

一般选择lock.lock(10, TimeUnit.SECONDS);设置指定过期时间

4.3 读写锁测试

创建writeValuereadValue方法,路径:com/atguigu/gulimall/product/web/IndexController.java

读读共享、读写互斥、写写互斥

@Autowired
StringRedisTemplate stringRedisTemplate;

@GetMapping("/write")
@ResponseBody
public String writeValue() {
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
    String s = "";
    RLock wLock = readWriteLock.writeLock();
    wLock.lock();
    try {
        s = UUID.randomUUID().toString();
        Thread.sleep(30000);
        stringRedisTemplate.opsForValue().set("writeValue", s);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        wLock.unlock();
    }

    return s;
}

@GetMapping("/read")
@ResponseBody
public String readValue() {
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = readWriteLock.readLock();
    rLock.lock();
    try {
        s = stringRedisTemplate.opsForValue().get("writeValue");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }

    return s;
}

4.4 闭锁测试

创建lockDoorgogogo方法,路径:com/atguigu/gulimall/product/web/IndexController.java

@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.trySetCount(5);
    door.await(); // 等待闭锁完成

    return "放假了...";
}

@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.countDown();

    return id + "班的人都走了...";
}

4.5 信号量测试

1、创建parkgo方法,路径:com/atguigu/gulimall/product/web/IndexController.java

@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
    RSemaphore park = redissonClient.getSemaphore("park");
    // park.acquire(); // 获取一个信号
    boolean b = park.tryAcquire();
    return "ok -> " + b;
}

@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release(); // 释放一个车位
    return "ok";
}

2、在redis里添加一个缓存

1

3、访问park和go进行测试

5 缓存一致性解决

5.1 双写模式

即修改数据库后在修改缓存。

存在的问题:

假如一个修改数据库的请求 P1 进来,修改了数据库里的记录 A -> B,但由于某些原因,cpu开始执行另一个修改数据库的请求,把记录 B 又改回了 A,然后先执行了 P2 的写缓存。最后在执行 P1 的写缓存。最终导致数据库的数据和缓存的数据不一致,数据库里的记录是 A,而缓存里的记录是 B

image-20220517161520085

5.2 失效模式

即修改完数据库某个数据后,删除缓存里的对应的数据内容

存在的问题:

读写如果并发执行,可能读到的是脏数据

image-20220517161616497

5.3 解决方案分析

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

  1. 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  2. 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  3. 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  4. 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

总结:

  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

我们最终选择失效模式 + 读写锁的方式

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值