谷粒商城左侧导航栏缓存问题

  • Post author:
  • Post category:其他



1、为何要引入缓存?

防止大量请求直接访问数据库,导致数据库压力过大,从而崩溃

访问数据库耗时太久,直接访问缓存可以加快访问速度


2、缓存

本地缓存—-直接使用map将数据库查到的缓存放到map中,请求过来后先去map中查key有的化返回

缺点:对于分布式系统来说,map只能存在单台服务其中,当网关路由到其他服务器时,其他服务器还需要在查一遍数据库并放入缓存

分布式缓存—引入其他中间件做缓存管理,将所有服务器的缓存都放入在中间件中—redis

redist整合:引入starter 配置post和host 使用stringredistemplate操作

       //依赖
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


yml
spring:
  redis:
    port: 6379
    host: 192.168.111.149



 stringRedisTemplate测试
    @Autowired
    StringRedisTemplate stringRedisTemplate;


    public void  testRedis(){
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("hello","w"+ UUID.randomUUID().toString());
    }

可能会遇到的问题:

**内存泄漏及解决办法** 当进行压力测试时后期后出现堆外内存溢出 OutOfDirectMemoryError 产生原因: 1)、springboot2.0 以后默认使用 lettuce 操作 redis 的客户端,它使用通信 2)、lettuce 的 bug 导致 netty 堆外内存溢出 解决方案:由于是 lettuce 的 bug 造成,不能直接使用-Dio.netty.maxDirectMemory 去调大虚拟机堆外内存 1)、升级 lettuce 客户端。 2)、切换使用 jedis

3、高并发下的缓存问题

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

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

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

4、缓存加锁解决缓存击穿问题

(1)本地缓存

直接使用lock或synchronized (this)【因为springboot每个服务器只有一个实例,所以只有一把锁】将查询 db 的方法加锁

缺点:只能锁住单个服务器,不能锁定全部服务器,导致每个服务器都要去查一遍db

(2)redis缓存

使用setnx进行上锁,谁抢到赋值权谁就拿到锁,服务结束后在释放锁

问题:如何保证释放的是该线程的锁

       //上锁时,v设定一个uuid 和一个过期时间,释放锁时查看两者是否相等
 String s = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", s,300,TimeUnit.SECONDS);

//保证上锁和解锁的原子行,防止出现突然停机等问题导致锁无法释放,造成死锁现象

                //要使用脚本删锁
                String script = "if redis.call(‘get’,KEYS[1]) == ARGV[1] then return redis.call(‘del’,KEYS[1])" +
                        "else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),s);


问题:
1、setnx 占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就
造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除
2、setnx 设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis 支持使用 setnx ex 命令
3、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为 uuid,每个人匹配是自己的锁才删除。
4、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那
么我们删除的是别人的锁
解决:
删除锁必须保证原子性。使用 redis+Lua 脚本完成

不足:无法解决自动续期(业务时间超过过期时间导致锁被提前释放)

(3)加锁和释放锁的时序问题

1、加锁 2、执行业务逻辑 3、更新缓存 4、释放锁

5、整合redisson解决自动续期

/**
 * 整合redisson作为分布式锁等功能框架
 * 1、配置redisson
 * 2、redission优势:解决了超长业务的自动续期30s,不用担心事务时间长事务锁被自动删除
 * 加锁的业务只要完成就不会在此续期,即使不手动解锁也会在默认30s后山丘
 */
@Configuration
public class MyRedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();

        config.useSingleServer().setAddress("redis://192.168.111.149:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

redisson测试及说明

  @GetMapping("hello")
    @ResponseBody
    public String hello() {
        //1、只要同一个名字就是统一把锁
        //如果指定了到期时间,则即使业务没有执行完,看门狗也不会自动续期,而直接释放锁
        //原理:如果我们传入时间,redisson直接执行lua脚本,进行占锁,超时释放
        //如果未指定,默认传入-1,就是用30s(lockwatchTimeOut看门狗默认时间),只要占锁成功,就会开启一个定时任务(重新给锁设置过期时间30s){定时时长为10s}
        RLock lock = redissonClient.getLock("lock");
        //2、加锁
        /**
         * 最佳实战:lock.lock(30, TimeUnit.SECONDS);延长时间  省去看门狗的调度
         */
        lock.lock();//阻塞等待
        try {
        System.out.println("加锁成功"+Thread.currentThread().getId());}
        finally {
            //解锁
            lock.unlock();
            System.out.println("解锁"+Thread.currentThread().getId());
        }

        return "hello";
    }

6、springcache的整合

/**
 * 整合spring cache简化缓存开发
 * 以入依赖  redis 和 springcache
 * 配置redis 作为缓存
 * 开启缓存功能
 * 只需要使用注解就能完成缓存操作
 * 1)、读模式
 * 缓存穿透:查询一个 null 数据。解决方案:缓存空数据,可通过
 * `spring.cache.redis.cache-null-values=true`
 * 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
 * 使用 sync = true 来解决击穿问题
 * 缓存雪崩:大量的 key 同时过期。解决:加随机时间。加上过期时间
 * 2)、写模式:(缓存与数据库一致)
 * a、读写加锁。
 * b、引入 Canal,感知到 MySQL 的更新去更新 Redis
 * c 、读多写多,直接去数据库查询就行
 * 3)、总结:
 * 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用 Spring-Cache):
 * 写模式(只要缓存的数据有过期时间就足够了)
 * 特殊数据:特殊设计
 */



版权声明:本文为weixin_49260963原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。