分布式缓存解决方案

高并发首选:缓存

项目代码下载地址:https://gitee.com/tyytx/distrbute-demo.git
接口高并发的解决思路:1、加缓存 2、数据静态化 3、集群 4、分布式 5、同步转异步 6、限流、降级
适合加缓存的场景:读多写少的数据,不经常需要修改的数据、一致性要求不高(数据只能保持最终一致性,不能保证数据同步一致性)

缓存的概念

1)外存

外存储器是指除计算机内存及CPU缓存以外的存储器,断电后仍然能保存数据。常用的有硬盘、u盘等。

2)内存

内存是计算机组成部分。被称为内存存储器,其作用是暂时存放CPU运算数据,以及与外存之间交互的存储器。只要计算机在运行中,CPU就会把需要运算的数据读到内存中,当运算完成后CPU在将运算结果写到外存中。断电后数据就会清空,常用的有内存条。

3)缓存

缓存就是介于内存和外存之间的存储,加快内存和外存之间的数据交互。软件中间件一般用外部计算机内存当缓存。

4)缓存的指标

1)缓存命中率
从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。
2)缓存更新策略
如果缓存满了,从缓存中清除数据的策略。常见的有:LFU、LRU、FIFO
LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除。
LFU(Least Frequencyly Used):最少使用算法,一定时间使用次数最少的那个被移除。
FIFO(First In First Out):先进先出算法,先放入缓存的先被移除。

Redis缓存在Java中的实现

在java代码中,我们一般对调用的方法进行缓存控制,比如我要查询具体的省市区县数据,findProvidenceAndCityAndArea(String id),那么我们应该在调用这个方法之前先从缓存中查找有没有这个数据,如果没有在调用该方法从数据库中查询,然后添加到缓存中去;下次接口调用时将会从缓存直接获取数据。Java中使用分布式缓存Redis。

1)缓存逻辑流程图

查询策略:
在这里插入图片描述
修改策略:
在这里插入图片描述

2)逻辑流程代码

查询代码:

/**
     * 使用缓存,根据省份ID获取省市数据
     * @param provinceid
     * @return
     */
    @Override
    public Provinces detail(String provinceid) {
        Provinces provinces = null;
        //1、在redis查询
        provinces = (Provinces)redisTemplate.opsForValue().get(provinceid);
        //2.如果命中,则返回缓存数据
        if (null != provinces){
            //redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);
            System.out.println("缓存中得到数据");
            return provinces;
        }
        //3.如果没有命中,则去数据库中读取数据
        provinces = super.detail(provinceid);
        if (null != provinces){
            //4.将查询出来的数据写入缓存
            redisTemplate.opsForValue().set(provinceid,provinces);//set缓存
            //redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);//设置过期
        }
        return provinces;
    }

修改策略采用双删策略:

@Override
    public Provinces update(Provinces entity) {//双删,防止多线程操作之间的时间间隔导致数据不一致
        redisTemplate.delete(entity.getProvinceid());//直接删除缓存,预防数据库成功,缓存失败
        super.update(entity);
        redisTemplate.delete(entity.getProvinceid());//双删
        return entity;
    }

    @Override
    public Provinces add(Provinces entity) {
        redisTemplate.delete(entity.getProvinceid());
        super.add(entity);
        redisTemplate.delete(entity.getProvinceid());//双删
        return entity;
    }

    @Override
    public void delete(String provinceid) {
        redisTemplate.delete(provinceid);
        super.delete(provinceid);
        redisTemplate.delete(provinceid);//双删
    }

SpringCache的用法

因为缓存代码逻辑有一定的固定性,所以可以利用模板设计模式对其进行封装。SpringCache并非具体的缓存技术,而是对各种缓存中间件的封装,具体的底层缓存是EhCache还是Redis,只需要简单的配置就可以了。

设计理念

正如Spring框架的其他服务一样,Spring Cache首先是提供了一层抽象,和兴抽象主要体现在这俩个接口上:
在这里插入图片描述

org.springframework.cache.Cache: 代表缓存本身
org.springframework.cache.CacheManager:代表对缓存的处理和管理
抽象的意义在于屏蔽实现细节的差异和提供扩展性,Cache的抽象解耦了缓存的使用和缓存的后端存储,方便后续更换存储。

使用SpringCache

在这里插入图片描述
使用示例:
1)声明缓存:在方法上添加@cacheable等注解,表示缓存该方法的结果。

    @Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个
    public Provinces detail(String provinceid) {//一个接口方法,对应一个缓存器
        return super.detail(provinceid);
    }

2)开启Spring的cache功能
<cache:annotation-driven /> 或者使用注解@EnableCaching 的方式

@Configuration
@EnableCaching
public class CacheConfig {
}

3)配置后端的存储
1)JDK内存作为缓存

    @Bean
    public CacheManager cacheManager() {
        //jdk里,内存管理器
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province")));
        return cacheManager;
    }

RedisCacheManager

 @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager
                .builder(connectionFactory)
                .cacheDefaults(
                        RedisCacheConfiguration.defaultCacheConfig()
                                .entryTtl(Duration.ofSeconds(20))) //缓存时间绝对过期时间20s
                .transactionAware()
                .build();
    }

注解风格说明

1)@Cacheable:查询方法

@Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个
    public Provinces detail(String provinceid) {//一个接口方法,对应一个缓存器
        return super.detail(provinceid);
    }

2)@CachePut:新增或者修改方法

//这个AOP,先修改数据库,在删除缓存
    @CachePut(key = "#entity.provinceid")
    public Provinces update(Provinces entity) {
        return super.update(entity);
    }

3)@CacheEvict:删除缓存

@CacheEvict
    public void delete(String provinceid) {
        super.delete(provinceid);
    }

缓存穿透解决方案

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起Id为“-1”的数据或者id为不存在的数据。这是的用户很可能是恶意攻击,攻击会导致数据库压力过大。
在这里插入图片描述

解决方案:使用布隆过滤器

缓存击穿解决方案

缓存击穿是指缓存中没有,但是数据库中有的数据(一般是缓存过期),这是由于并发用户特别多,同时读取缓存没读取到数据,又同时去数据库读取数据,一期数据库压力瞬间增大,造成过大压力。
在这里插入图片描述

private BloomFilter<String> bf =null; //等效成一个set集合

    /**
     * 在bean初始化完成后,实例化bloomFilter,并加载数据
     */
    @PostConstruct //对象创建后,自动调用本方法
    public void init(){
        List<Provinces> provinces = this.list();
        //当成一个SET----- 占内存,比hashset占得小很多
        bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), provinces.size());// 32个
        for (Provinces p : provinces) {
            bf.put(p.getProvinceid());
        }
    }

    @Cacheable(value = "province")
    public Provinces detail(String provinceid) {
        //先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
        if(!bf.mightContain(provinceid)){
            System.out.println("非法访问--------"+System.currentTimeMillis());
            return null;
        }
        System.out.println("数据库中得到数据--------"+System.currentTimeMillis());
        Provinces provinces = super.detail(provinceid);

        return provinces;
    }

缓存雪崩解决方案

缓存血本是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查询同一条数据,缓存雪崩是不同数据同时过期,很多数据都要从数据库在这里插入图片描述
查询。

解决方案:加显示锁(可以解决缓存击穿问题)

public Provinces detail(String provinceid) {
        // 1.从缓存中取数据
        Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
        if (valueWrapper != null) {
            logger.info("缓存中得到数据");
            return (Provinces) (valueWrapper.get());
        }
        //2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁
        doLock(provinceid);//32个省,最多只有32把锁,1000个线程

        try{//第二个线程进来了
            // 一次只有一个线程
             //双重校验,不加也没关系,无非是多刷几次库
            valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值
            if (valueWrapper != null) {
                logger.info("缓存中得到数据");
                return (Provinces) (valueWrapper.get());//第二个线程,这里返回
            }
            Provinces provinces = super.detail(provinceid);
            // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
            if (null != provinces){
                cm.getCache(CACHE_NAME).put(provinceid, provinces);
            }
            return provinces;
        }catch(Exception e){
            return null;
        }finally{
            //4.解锁
            releaseLock(provinceid);
        }
    }

缓存的数据一致性解决策略

数据更新就会引起数据库的更新和缓存更新,就容易出现缓存(Redis)和数据库(Mysql)间的数据一致性问题。无论哪种解决方案都有缺点,没有完美的解决方案。
在这里插入图片描述

1)数据实时同步更新:代码同步调用Cache增删改

优点:数据实时同步更新,保持强一致性
缺点:代码耦合,对业务代码有侵入性

2)数据准实时更新:mq异步更新数据

优点:数据同步有较短延迟 ,与业务解耦
缺点:实现复杂,架构较重,中间件的稳定性

3)缓存失效机制:缓存失效

优点:实现简单,无须引入额外逻辑
缺点:有一定延迟,存在缓存击穿/雪崩问题

4)定时任务更新:定时任务,最终数据一致性

优点:不影响正常业务
缺点:不保证一致性,依赖定时任务

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值