【Gulimall+】分布式锁之redis实现、Redisson专业框架实现,整合SpringCache简化缓存开发


复制服务构建本地分布式
在这里插入图片描述
右键1,copy configuration -> 2改下端口

缓存与分布式锁

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问,而 db 承担数据落盘工作
哪些数据适合放入缓存?
即时性、数据一致性要求不高的
访问量大且更新频率不高的数据(读多、写少)

注意:在开发中,凡是放到缓存中的数据我们都应该制定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载的流程,避免业务奔溃导致的数据永久不一致的问题

启动redis服务

如果直接挂载的话docker会以为挂载的是一个目录,所以我们先创建一个文件然后再挂载,在虚拟机中。

#在虚拟机中
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf

docker pull redis

docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf


# 直接进去redis客户端。
docker exec -it redis redis-cli

fushall #清空redis所有缓存

# 设置容器自动启动
sudo docker update redis --restart=always

默认是不持久化的。在配置文件中输入appendonly yes,就可以aof持久化了。修改完docker restart redis,docker -it redis redis-cli

vim /mydata/redis/conf/redis.conf
# 插入下面内容
appendonly yes
docker restart redis

redis客户端UI

参考link,但是Ubuntu20.04已经预安装了snap,故直接:

sudo snap install redis-desktop-manager

在这里插入图片描述

整合 redis 作为缓存

1、引入依赖
SpringBoot 整合 redis,查看SpringBoot提供的 starts

 <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!--不加载自身的 lettuce-->
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

PS:为何不用自带的lettuce
1、SpringBoot2.0以后默认使用 Lettuce作为操作redis的客户端,它使用 netty进行网络通信
2、lettuce 的bug导致netty堆外内存溢出,-Xmx300m netty 如果没有指定堆内存移除,默认使用 -Xmx300m

  • 解决方案:升级 lettuce客户端,或切换使用jedis

2、配置

#spring.redis.host=localhost
#spring.redis.port=6379

3、使用

@Autowired
StringRedisTemplate redisTemplate;

高并发下缓存失效问题

1、缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null结果缓存,并加入短暂过期时间

2、缓存雪崩:
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时
压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体
失效的事件。

3、缓存击穿:
• 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
• 如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:加锁
大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

在product的页面index.html加载时,会自动发送个

 $.getJSON("index/catalog.json",function (data) {
 ...
}

com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java

 public Map<String, List<Catelog2Vo>> getCatelogJson2() {  //原始的getCatelogJson

        // 给缓存中放 json 字符串、拿出的是 json 字符串,还要逆转为能用的对象类型【序列化和反序列化】

        // 1、加入缓存逻辑,缓存中放的数据是 json 字符串
        // JSON 跨语言,跨平台兼容
        String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
        if (StringUtils.isEmpty(catelogJSON)) {
            // 2、缓存没有,从数据库中查询
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDBWithRedissonLock();
            // 3、查询到数据,将数据转成 JSON 后放入缓存中
//            String s = JSON.toJSONString(catelogJsonFromDb);  //移到getDataFromDb中
//            redisTemplate.opsForValue().set("catelogJSON",s);
            return catelogJsonFromDb;
        }
        System.out.println("缓存命中catelogJSON");
        // 转换为我们指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });

        return result;

    }

本地锁

    public synchronized Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithLocalLock() { //用this加锁 PS:springboot所有组件在容器中都是单例的

        return getDataFromDb();

    }

在这里插入图片描述

 private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
        if (!StringUtils.isEmpty(catelogJSON)) {
            System.out.println("准备查询,但又缓存命中");
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        Map<String, List<Catelog2Vo>> categoryList = getStringListMap();

        // 2、放进redis缓存后再释放锁
        String s = JSON.toJSONString(categoryList);
        redisTemplate.opsForValue().set("catelogJSON", s);

        return categoryList;
    }

在这里插入图片描述

分布式锁

Redis实现

将键key设定为指定的“字符串”值。如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当set命令执行成功之后,之前设置的过期时间都将失效。set命令选项如下:
在这里插入图片描述

 public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
        String uuid = UUID.randomUUID().toString();
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0");
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111",300, TimeUnit.SECONDS);
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        //底层实现 SET lock uuid EX 300 NX整个是原子的
        if (lock) {
            System.out.println("获取分布式锁成功");
            // 加锁成功..执行业务  意外情况:还未设置过期时间 宕机了,就成了死锁
            // 设置过期时间,必须和加锁是同步的,原子的
//            redisTemplate.expire("lock",30, TimeUnit.SECONDS); // 设置过期时间
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try {
                dataFromDb = getDataFromDb();
            } finally {

//            redisTemplate.delete("lock"); // 删除锁
//            String lock1 = redisTemplate.opsForValue().get("lock");
//            if (lock1.equals(uuid)) {
//			意外情况:A还未开始删除,该锁正好失效了,且该线程的时间片也没了,切换到B线程,B时间片用完,回到A继续执行,可能会将B的锁删掉
//                // 删除我自己的锁 
//                redisTemplate.delete("lock"); // 删除锁
//            }
                // 通过使用lua脚本进行原子性删除
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                //删除锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            System.out.println("获取分布式锁失败,等待重试");
            // 加锁失败,重试 synchronized()
            // 休眠100ms重试
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatelogJsonFromDBWithRedisLock(); //自旋
        }

    }
Redisson专业框架实现

1、导入依赖

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

2、配置类
com/atguigu/gulimall/product/config/MyRedissonConfig.java

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

        return redissonClient;
    }
}

3、实现

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

        RLock rLock = redisson.getLock("catelogJson-lock");
        rLock.lock(300,TimeUnit.SECONDS);

        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            rLock.unlock();
        }
        return dataFromDb;
    }
Redisson - Lock 看门狗原理
@RequestMapping("/hello")
@ResponseBody
public String hello(){
    // 1、获取一把锁,只要锁得名字一样,就是同一把锁
    RLock lock = redission.getLock("my-lock");

    // 2、加锁
    lock.lock(); // 阻塞式等待,默认加的锁都是30s时间
    // 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期后被删掉
    // 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s以后自动删除

    lock.lock(10, TimeUnit.SECONDS); //10s 后自动删除
    //问题 lock.lock(10, TimeUnit.SECONDS) 在锁时间到了后,不会自动续期
    // 1、如果我们传递了锁的超时时间,就发送给 redis 执行脚本,进行占锁,默认超时就是我们指定的时间
    // 2、如果我们为指定锁的超时时间,就是用 30 * 1000 LockWatchchdogTimeout看门狗的默认时间、
    //      只要占锁成功,就会启动一个定时任务,【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s就自动续期
    //      internalLockLeaseTime【看门狗时间】 /3,10s

    //最佳实践
    // 1、lock.lock(10, TimeUnit.SECONDS);省掉了整个续期操作,手动解锁

    try {
        System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
        Thread.sleep(3000);
    } catch (Exception e) {

    } finally {
        // 解锁 将设解锁代码没有运行,reidsson会不会出现死锁
        System.out.println("释放锁...." + Thread.currentThread().getId());
        lock.unlock();
    }

    return "hello";
}
Reidsson的部分锁

1、Reidsson - 读写锁

/**
     * 保证一定能读取到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享锁)读锁是一个共享锁
     * 写锁没释放读锁就必须等待
     * 读 + 读 相当于无锁,并发读,只会在 reids中记录好,所有当前的读锁,他们都会同时加锁成功
     * 写 + 读 等待写锁释放
     * 写 + 写 阻塞方式
     * 读 + 写 有读锁,写也需要等待
     * 只要有写的存在,都必须等待
     * @return String
     */
    @RequestMapping("/write")
    @ResponseBody
    public String writeValue() {

        RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
        String s = "";
        RLock rLock = lock.writeLock();
        try {
            // 1、改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功..." + Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            redisTemplate.opsForValue().set("writeValue",s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("写锁释放..." + Thread.currentThread().getId());
        }
        return s;
    }

    @RequestMapping("/read")
    @ResponseBody
    public String readValue() {
        RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
        RLock rLock = lock.readLock();
        String s = "";
        rLock.lock();
        try {
            System.out.println("读锁加锁成功..." + Thread.currentThread().getId());
            s = (String) redisTemplate.opsForValue().get("writeValue");
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("读锁释放..." + Thread.currentThread().getId());
        }
        return s;
    }

2、Redisson - 信号量测试

/**
 * 车库停车
 * 3车位
 * @return
 */
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
    RSemaphore park = redission.getSemaphore("park");
    boolean b = park.tryAcquire();//获取一个信号,获取一个值,占用一个车位

    return "ok=" + b;
}

@GetMapping("/go")
@ResponseBody
public String go() {
    RSemaphore park = redission.getSemaphore("park");

    park.release(); //释放一个车位

    return "ok";
}

类似 JUC 中的 Semaphore

3、Redisson - 闭锁测试

/**
 * 放假锁门
 * 1班没人了
 * 5个班级走完,我们可以锁们了
 * @return
 */
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redission.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();//等待闭锁都完成

    return "放假了....";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redission.getCountDownLatch("door");
    door.countDown();// 计数器减一

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

和 JUC 的 CountDownLatch 一致
await()等待闭锁完成
countDown() 把计数器减掉后 await就会放行

缓存数据一致性

缓存数据一致性 - 双写模式:写完数据库紧接写缓存
可能出现的问题
在这里插入图片描述两个线程写 由于卡顿等原因,导致写缓存1在写缓存2后面,后写成功的会把之前写的数据给覆盖,就出现了不一致,这就会造成脏数据

缓存数据一致性 - 失效模式:写完数据库删对应缓存,读的时候若缓存不存在从数据库加载到缓存
可能出现的问题
在这里插入图片描述一号连接 写数据库 然后删缓存
二号连接 写数据库时网络连接慢,还没有写入成功
三号链接 直接读取数据,读到的是一号连接写入的数据,此时 二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存

缓存数据一致性解决方案

无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实力同时更新会出事,怎么办?
1、如果是用户纯度数据(订单数据、用户数据),这并发几率很小,几乎不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
2、如果是菜单,商品介绍等基础数据,也可以去使用 canal 订阅,binlog 的方式
3、缓存数据 + 过期时间也足够解决大部分业务对缓存的要求
4、通过加锁保证并发读写,写写的时候按照顺序排好队,读读无所谓,所以适合读写锁,(业务不关心脏数据,允许临时脏数据可忽略)

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

整合SpringCache简化缓存开发

1、引入依赖:spring-boot-starter-cache

 <!--Spring Cache-->
 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId> spring-boot-starter-cache</artifactId>
  </dependency>

2、写配置类

自动配置了那些 CacheAutoConfiguration会导入 RedisCacheConfiguration

com/atguigu/gulimall/product/config/MyCacheConfig.java

@EnableConfigurationProperties(CacheProperties.class) //将CacheProperties放容器中
@EnableCaching
@Configuration
public class MyCacheConfig {

    /**
     * 配置文件中的东西没有用上
     * 1、原来的配置吻技安绑定的配置类是这样子的
     *      @ConfigurationProperties(prefix = "Spring.cache")
     * 2、要让他生效
     *      @EnableConfigurationProperties(CacheProperties.class)
     * @param cacheProperties
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { //此处CacheProperties cacheProperties效果同自动注入
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置key的序列化
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        // 设置value序列化 ->JackSon
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中的所有配置都生效
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

3、配置使用redis作为缓存

spring:
  cache:
    type: redis  # 配置使用redis作为缓存
    redis:
      time-to-live: 3600000           # 过期时间 ms
#      key-prefix: CACHE_              # key前缀  没有设置默认以缓存名字作为前缀
      use-key-prefix: true            # 是否使用写入redis前缀
      cache-null-values: true         # 是否允许缓存空值  防止缓存穿透

4、原理
CacheAutoConfiguration ->RedisCacheConfiguration ->自动配置了 RedisCacheManager ->初始化所有的缓存 -> 每个缓存决定使用什么配置 ->如果redisCacheConfiguration有就用已有的,没有就用默认的->想改缓存的配置,只需要把容器中放一个 RedisCacheConfiguration 即可 ->就会应用到当前 RedisCacheManager管理所有缓存分区中

5、注解
对于缓存声明,Spring的缓存抽象提供了一组Java注解

/**
@Cacheable: Triggers cache population:触发将数据保存到缓存的操作
@CacheEvict: Triggers cache eviction: 触发将数据从缓存删除的操作
@CachePut: Updates the cache without interfering with the method execution:不影响方法执行更新缓存
@Caching: Regroups multiple cache operations to be applied on a method:组合以上多个操作
@CacheConfig: Shares some common cache-related settings at class-level:在类级别共享缓存的相同配置
**/

6、缓存穿透问题解决

    /**
     * 1、每一个需要缓存的数据我们都需要指定放到那个名字的缓存【缓存分区的划分【按照业务类型划分】】
     * 2、@Cacheable({"category"})
     *      代表当前方法的结果需要缓存,如果缓存中有,方法不调用
     *      如果缓存中没有,调用方法,最后将方法的结果放入缓存
     * 3、默认行为:
     *      1、如果缓存中有,方法不用调用
     *      2、key默自动生成,缓存的名字:SimpleKey[](自动生成的key值)
     *      3、缓存中value的值,默认使用jdk序列化,将序列化后的数据存到redis
     *      3、默认的过期时间,-1
     *
     *    自定义操作
     *      1、指定缓存使用的key     key属性指定,接收一个SpEl  https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/spring-framework-reference/integration.html#cache-spel-context
     *      2、指定缓存数据的存活时间  配置文件中修改ttl
     *      3、将数据保存为json格式
     * 4、Spring-Cache的不足:
     *      1、读模式:
     *          缓存穿透:查询一个null数据,解决 缓存空数据:ache-null-values=true
     *          缓存击穿:大量并发进来同时查询一个正好过期的数据,解决:加锁 ? 默认是无加锁;设置syn=true,加读锁解决缓存击穿
     *          缓存雪崩:大量的key同时过期,解决:加上随机时间,Spring-cache-redis-time-to-live
     *       2、写模式:(缓存与数据库库不一致)
     *          1、读写加锁
     *          2、引入canal,感知到MySQL的更新去更新数据库
     *          3、读多写多,直接去数据库查询就行
     *
     *    总结:
     *      常规数据(读多写少,即时性,一致性要求不高的数据)完全可以使用SpringCache 写模式( 只要缓存数据有过期时间就足够了)
     *
     *    特殊数据:特殊设计
     *      原理:
     *          CacheManager(RedisManager) -> Cache(RedisCache) ->Cache负责缓存的读写
     * @return
     */
@Cacheable(value = {"category"},key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson(){
        Map<String, List<Catelog2Vo>> categoryList = getStringListMap();

        return categoryList;
    }

在这里插入图片描述

7、缓存更新

    /**
     * 级联更新所有的关联数据
     * @CacheEvict 失效模式   再加上@CachePut就变成双写模式
     * 1、同时进行多种缓存操作 @Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
     * 3、存储同一类型的数据,都可以指定成同一分区,分区名默认就是缓存的前缀
     *
     * @param category
     */

    @Caching(
            evict = {@CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),  //注意key如果不是spel 则需要有单引号'getLevel1Categorys'
                    @CacheEvict(value = {"category"}, key = "'getCatelogJson'")  //修改失效清除,key相同方可, 前缀自动加
            })
    //    @CacheEvict(value = {"category"},allEntries = true)
    @Transactional  //为了保持两者更新同步  在MyBatisConfig配置类中开启了事务
    @Override
    public void updateDetails(CategoryEntity category) {
        this.updateById(category); //更新自己
        if (!category.getName().isEmpty()) {
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
    }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空•物语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值