缓存和分布式锁

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 缓存和分布式锁

缓存的使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。


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

本地缓存

在单个机器中可以把缓存存到本地


但是在分布式系统下


这样会造成每个机器的缓存不一致,会有分布式缓存读写的问题

分布式缓存

把缓存都放在中间件里,统一操作一个缓存


缓存redis

这里操作环境为商品服务模块,虚拟机初始化时已安装 Redis。

引入Redis

<!-- redis -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

YML

spring:
  redis:
    host: 192.168.195.100
    port: 6379

测试联通

@Autowired
    StringRedisTemplate stringRedisTemplate;
    @Test
    public  void  teststringRedis(){
        ValueOperations<String,String> operations=stringRedisTemplate.opsForValue();
        operations.set("hello","world"+ UUID.randomUUID());
        String value=operations.get("hello");
        System.out.println(value);
    }

缓存三级菜单的数据

这里使用依赖中自带的 StringRedisTemplate 来操作 Redis。这里存储的值为转化成 JSON 字符串的对象信息。

核心代码

@Autowired
    StringRedisTemplate redisTemplate;
@Override
    public   Map<String, List<Catalogs2Vo>> getcatalogJson()
    {
        String value=redisTemplate.opsForValue().get("categoryJson");
        if(StringUtil.isNullOrEmpty(value))
        {
            Map<String, List<Catalogs2Vo>> catalogJsonFromDB = getcatalogJsonFromDB();
            redisTemplate.opsForValue().set("categoryJson", JSON.toJSONString(catalogJsonFromDB));
            return catalogJsonFromDB;
        }
        Map<String, List<Catalogs2Vo>> listMap=JSON.parseObject(value,new TypeReference<Map<String, List<Catalogs2Vo>>>(){});
        return  listMap;
    }

堆外内存溢出异常:

这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。

下面进行分析:

  • SpringBoot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信;
  • lettuce 的 bug 导致 netty 堆外内存溢出;
  • netty 如果没有指定堆外内存,默认使用 -Xmx 参数指定的内存;
  • 可以通过 -Dio.netty.maxDirectMemory 进行设置;

解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。

  • 升级 lettuce 客户端,或使用 jedis 客户端
<!-- redis -->
<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>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>

缓存失效问题

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞,数据库瞬时压力增大,最终导致崩溃

解决:缓存空结果、并且设置短的过期时间。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失 效,请求全部转发到 DB,DB 瞬时压力过重雪崩

解决

原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。

这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。

解决:

加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。

加锁解决缓存击穿问题

在主方法加锁之后,

原子性操作:

1.确认缓存

2.查询数据库

3.以及将结果放入缓存

否者会导致释放锁的时序问题

public Map<String, List<Catalogs2Vo>>  getcatalogJsonFromDB() {
        synchronized (this)
        {
            String value=redisTemplate.opsForValue().get("categoryJson");
            //得到锁以后我没应该再去缓存中确定一次,没有再查询
            if(!StringUtil.isNullOrEmpty(value)) {
                //缓存不为空直接返回
                Map<String, List<Catalogs2Vo>> listMap=JSON.parseObject(value,new TypeReference<Map<String, List<Catalogs2Vo>>>(){});
                return  listMap;
            }
            System.out.println("查询了数据库....");
            // 性能优化:将数据库的多次查询变为一次
            List<CategoryEntity> selectList = this.baseMapper.selectList(null);
            //1、1)查出所有一级分类
            List<CategoryEntity> level1Category=selectLevel1Category();
            //封装数据
            Map<String, List<Catalogs2Vo>> parentCid  = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //每一个一级分类  查一级分类的耳机分类
                List<CategoryEntity> categoryEntities =  getParentCid(selectList, v.getCatId());
                List<Catalogs2Vo> catalogs2Vos = null;
                if (categoryEntities != null) {
                    catalogs2Vos = categoryEntities.stream().map(l2 -> {
                        Catalogs2Vo catalogs2Vo = new Catalogs2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> leve3 =getParentCid(selectList, l2.getCatId());
                        if (leve3 != null) {
                            List<Catalogs2Vo.Category3Vo> collect = leve3.stream().map(l3 -> {
                                //2、封装成指定格式
                                Catalogs2Vo.Category3Vo category3Vo = new Catalogs2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                return category3Vo;
                            }).collect(Collectors.toList());
                            catalogs2Vo.setCatalog3List(collect);
                        }
                        return catalogs2Vo;
                    }).collect(Collectors.toList());
                }
                return catalogs2Vos;
            }));
            redisTemplate.opsForValue().set("categoryJson", JSON.toJSONString(parentCid),1, TimeUnit.DAYS);
            return parentCid;
        }
    }
    private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList, Long parentCid) {
        return selectList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }
    @Autowired
    StringRedisTemplate redisTemplate;
    @Override
    public   Map<String, List<Catalogs2Vo>> getcatalogJson()
    {
        String value=redisTemplate.opsForValue().get("categoryJson");
        if(StringUtil.isNullOrEmpty(value))
        {
            System.out.println("缓存没命中....查询数据库");
            Map<String, List<Catalogs2Vo>> catalogJsonFromDB = getcatalogJsonFromDB();
            return catalogJsonFromDB;
        }
        System.out.println("缓存命中...");
        Map<String, List<Catalogs2Vo>> listMap=JSON.parseObject(value,new TypeReference<Map<String, List<Catalogs2Vo>>>(){});
        return  listMap;
    }


本地锁在分布式下的问题

分别启动四个商品服务,利用nginx转发到网关负载均衡访问10000、10001、10002端口


请求结果,服务都查询了一次数据库


本地锁只能锁住当前服务,其他服务同时进来也会同时查询,为了锁住所有的数据那么需要加一个分布式锁

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
缓存 算法 NoSQL
【分布式详解】一致性算法、全局唯一ID、分布式锁、分布式事务、 分布式缓存、分布式任务、分布式会话
分布式系统通过副本控制协议,使得从系统外部读取系统内部各个副本的数据在一定的约束条件下相同,称之为副本一致性(consistency)。副本一致性是针对分布式系统而言的,不是针对某一个副本而言。强一致性(strong consistency):任何时刻任何用户或节点都可以读到最近一次成功更新的副本数据。强一致性是程度最高的一致性要求,也是实践中最难以实现的一致性。单调一致性(monotonic consistency):任何时刻,任何用户一旦读到某个数据在某次更新后的值,这个用户不会再读到比这个值更旧的值。
646 0
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
2月前
|
缓存 NoSQL Java
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
缓存与分布式锁、Redisson分布式锁、缓存数据一致性【必须满足最终一致性】
128 14
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
4月前
|
canal 缓存 NoSQL
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;先删除缓存还是先修改数据库,双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
|
4月前
|
存储 缓存 NoSQL
高并发架构设计三大利器:缓存、限流和降级问题之Redis用于搭建分布式缓存集群问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Redis用于搭建分布式缓存集群问题如何解决
|
4月前
|
存储 缓存 数据库
分布式篇问题之全量缓存解决数据库和缓存的一致性问题如何解决
分布式篇问题之全量缓存解决数据库和缓存的一致性问题如何解决
|
4月前
|
缓存 Devops 微服务
微服务01好处,随着代码越多耦合度越多,升级维护困难,微服务技术栈,异步通信技术,缓存技术,DevOps技术,搜索技术,单体架构,分布式架构将业务功能进行拆分,部署时费劲,集连失败如何解决
微服务01好处,随着代码越多耦合度越多,升级维护困难,微服务技术栈,异步通信技术,缓存技术,DevOps技术,搜索技术,单体架构,分布式架构将业务功能进行拆分,部署时费劲,集连失败如何解决
|
6月前
|
存储 缓存 NoSQL
软件体系结构 - 缓存技术(4)Redis分布式存储
【4月更文挑战第20天】软件体系结构 - 缓存技术(4)Redis分布式存储
86 12
|
5月前
|
存储 缓存 NoSQL
了解Redis,第一弹,什么是RedisRedis主要适用于分布式系统,用来用缓存,存储数据,在内存中存储那么为什么说是分布式呢?什么叫分布式什么是单机架构微服务架构微服务的本质
了解Redis,第一弹,什么是RedisRedis主要适用于分布式系统,用来用缓存,存储数据,在内存中存储那么为什么说是分布式呢?什么叫分布式什么是单机架构微服务架构微服务的本质