Redis实战 - 12 Redis 实现餐厅缓存功能

1. 缓存简介

1. 缓存概念

缓存是介于应用程序和物理数据源之间,其作用是为了降低应用程序对物理数据源访问的频次,从而提高了应用的运行性能。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。比如我们通常是直接查询MySQL数据库,那在高并发情况下,大量查询MySQL数据库会导致数据库性能变慢,因此我们在应用层与MySQL之间搭建一个Cache层,让请求先访问Cache,这样就能大大降低数据库的压力,提高性能。

2. 分布式缓存

在分布式系统开发中,系统与系统之间都属于进程级别,缓存系统也能跨进程叫分布式缓存,市面上分布式缓存技术有Memcached和Redis这两种。二者区别大致如下:

① 性能

性能上都很出色,具体到细节,由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100K以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起 Memcached,还是稍有逊色。

② 内存空间和数据量大小

MemCached可以修改最大内存,采用LRU算法。Redis增加了VM的特性,突破了物理内存的限制。

③ 操作比较

MemCached数据结构单一,仅用来缓存数据,而Redis支持更加丰富的数据类型,也可以在服务器端直接对数据进行丰富的操作,这样可以减少网络IO次数和数据体积。

④ 可靠性

MemCached不支持数据持久化,断电或重启后数据消失,但其稳定性是有保证的。Redis支持数据持久化和数据恢复,允许单点故障,但是同时也会付出性能的代价。

⑤ 应用场景

Memcached:动态系统中减轻数据库负载,提升性能;做缓存,适合多读少写。
Redis:适用于对读写效率要求都很高,数据处理业务复杂和对安全性要求较高的系统。

⑥ 需要慎重考虑的部分

Memcached单个key-value大小有限,一个value最大只支持1MB,而Redis最大支持512MB

Memcached只是个内存缓存,对可靠性无要求;而Redis更倾向于内存数据库,因此对对可靠性方
面要求比较高

从本质上讲,Memcached只是一个单一key-value内存Cache;而Redis则是一个数据结构内存数据
库,支持多种数据类型,因此Redis除单纯缓存作用外,还可以处理一些简单的逻辑运算,Redis不
仅可以缓存,而且还可以作为数据库用

Redis还可以集群分布式,也就是说集群本身均衡客户端请求,各个节点可以交流,可拓展行、可
维护性更强大。而Memcached的集群则通过第三方才能搭建

3. 存储方式

使用Redis做缓存的话,数据的存储结构有两种,一种采用strings存储,另外使用hashes存储。那使用哪种更好呢?得具体情况具体分析:

strings存储比较简单的,固定的数据,比如存储一个简单的用户信息(用户名、昵称、头像、年龄等)。存储时需要将数据进行序列化,获取时要反序列化。在数据量较小的情况下还是可以忽略这种开销的,但如果存储的的数据可能某些属性会有些变化,比如餐厅数据中,它有likeVotes(喜欢)和dislikeVotes(不喜欢)的数量,这类变的数据,那么我们采用hashes会更好,而且存储的时候没有序列化开销,官方推荐使用hashes

2. 创建 ms-restaurants

① 导入依赖

<dependencies>
    <!-- eureka client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- spring web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- spring data redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <!-- commons 公共项目 -->
    <dependency>
        <groupId>com.imooc</groupId>
        <artifactId>commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- swagger -->
    <dependency>
        <groupId>com.battcn</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
    </dependency>
    <!-- 单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

② 配置文件:

server:
  port: 8087 # 端口

Spring:
  application:
    name: ms-restaurants # 应用名
  # 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/orgnization?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
  # Redis
  redis:
    port: 6379
    host: 192.168.38.22
    timeout: 3000
    database: 1

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/

service:
  name:
    ms-oauth-server: http://ms-oauth2-server/
    ms-diners-server: http://ms-diners/

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰映射

③ 启动类:

@MapperScan("com.hh.restaurants.mapper")
@SpringBootApplication
public class RestaurantApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestaurantApplication.class, args);
    }
}

④ RestTemplateConfiguration 配置类:

@Configuration
public class RestTemplateConfiguration {

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
        restTemplate.getMessageConverters().add(converter);
        return restTemplate;
    }
    
}

⑤ RedisTemplateConfiguration 配置类:

@Configuration
public class RedisTemplateConfiguration {

    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key和value的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

3. 数据库表

创建 t_restaurants 数据表,同时导入21W餐厅数据(数据进行了脱敏),直接运行 t_restaurants.sql SQL文件即可。

在这里插入图片描述

CREATE TABLE `t_restaurants` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`name`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '餐厅英文名称' ,
`cnname`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '餐厅中文名称' ,
`x`  double NULL DEFAULT NULL COMMENT '经度' ,
`y`  double NULL DEFAULT NULL COMMENT '纬度' ,
`location`  varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '英文位置信息' ,
`cnlocation`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '中文位置信息' ,
`area`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商圈,比如徐家汇' ,
`telephone`  varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '餐厅电话' ,
`email`  varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '餐厅邮箱' ,
`website`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '餐厅官网' ,
`cuisine`  varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜系' ,
`average_price`  varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '均价' ,
`introduction`  varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '介绍' ,
`thumbnail`  varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '缩略图' ,
`like_votes`  int(10) NULL DEFAULT 0 COMMENT '喜欢数量' ,
`dislike_votes`  int(10) NULL DEFAULT 0 COMMENT '不喜欢数量' ,
`city_id`  int(11) NULL DEFAULT 21 COMMENT '城市id' ,
`is_valid`  int(1) NULL DEFAULT 1 COMMENT '1=Valid 0=Invalid' ,
`create_date`  datetime NULL DEFAULT NULL ,
`update_date`  datetime NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX `index_isvalid` (`is_valid`) USING BTREE 
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=1
ROW_FORMAT=COMPACT
;

4. 数据库表实体类

/**
 * 实体对象公共属性
 */
@Getter
@Setter
public class BaseModel implements Serializable {
    private Integer id;
    private Date createDate;
    private Date updateDate;
    private int isValid;
}
@Getter
@Setter
@ToString
public class Restaurant extends BaseModel {
    @ApiModelProperty("英文名称")
    private String name;
    
    @ApiModelProperty("中文名称")
    private String cnName;
    
    @ApiModelProperty("纬度")
    private Float x;
    
    @ApiModelProperty("经度")
    private Float y;
    
    @ApiModelProperty("位置-英文")
    private String location;
    
    @ApiModelProperty("位置-中文")
    private String cnLocation;
    
    @ApiModelProperty("商圈")
    private String area;
    
    @ApiModelProperty("电话")
    private String telephone;
    
    @ApiModelProperty("邮箱")
    private String email;
    
    @ApiModelProperty("官网")
    private String website;
    
    @ApiModelProperty("菜系")
    private String cuisine;
    
    @ApiModelProperty("均价,不显示具体金额")
    private String averagePrice;
    
    @ApiModelProperty("介绍")
    private String introduction;
    
    @ApiModelProperty("缩略图")
    private String thumbnail;
    
    @ApiModelProperty("喜欢")
    private int likeVotes;
    
    @ApiModelProperty("不喜欢")
    private int dislikeVotes;
    
    @ApiModelProperty("城市")
    private Integer cityId;
}

5. 添加餐厅到缓存

将餐厅添加到缓存时有全量添加增量添加

在某些特殊情况,比如初始化数据或缓存出现异常,没有将数据进行同步时,这时需要进行全量的数据同步。全量同步方式有两种,一种是逐条插入,另一种是批量插入,下面使用单元测试对两种方式进行对比。

1. 全量添加-逐条插入

① RestaurantMapper:

public interface RestaurantMapper {
    // 查询餐厅信息
    @Select("select id, name, cnName, x, y, location, cnLocation, area, telephone, " +
            "email, website, cuisine, average_price, introduction, thumbnail, like_votes," +
            "dislike_votes, city_id, is_valid, create_date, update_date" +
            " from t_restaurants")
    List<Restaurant> findAll();
}

② 单元测试:

package com.hh.restaurants;

@SpringBootTest
public class RestaurantApplicationTest {

}

逐条插入就是遍历数据集,使用redisTemplate.opsForHash().putAll(key, restaurantMap);插入即可

package com.hh.restaurants.service;

@Slf4j
public class RestaurantTest extends RestaurantApplicationTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RestaurantMapper restaurantMapper;

    // 逐行插入
    @Test
    void testSyncForHash() {
        // 查询所有的餐厅
        List<Restaurant> restaurants = restaurantMapper.findAll();
        long start = System.currentTimeMillis();
        restaurants.forEach(restaurant -> {
            // 将每个对象转为 Map<String, Object>
            Map<String, Object> restaurantMap = BeanUtil.beanToMap(restaurant);
            String key = RedisKeyConstant.restaurants.getKey() + restaurant.getId();
            redisTemplate.opsForHash().putAll(key, restaurantMap);
        });
        long end = System.currentTimeMillis();
        log.info("执行时间:{}", end - start); // 执行时间:94218
    }
}

2. 全量添加-Pipeline管道批量插入

批量插入使用pipeline指令完成。

Redis 是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。当请求进来以后,都是经过服务器进行返回,如果服务器没有响应时,其他请求进入等待。这时服务器也无法处理新请求,那有没有办法解决呢?

使用管道就能解决,即:将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。这种技术应用非常广泛比如MySQL的批量插入就如此。

package com.hh.restaurants.service;

@Slf4j
public class RestaurantTest extends RestaurantApplicationTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RestaurantMapper restaurantMapper;

    // Pipeline 管道插入
    @Test
    void testSyncForHashPipeline() {
        // 查询所有的餐厅
        List<Restaurant> restaurants = restaurantMapper.findAll();
        long start = System.currentTimeMillis();
        List<Long> list = redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            for (Restaurant restaurant : restaurants) {
                try {
                    String key = RedisKeyConstant.restaurants.getKey() + restaurant.getId();
                    Map<String, Object> restaurantMap = BeanUtil.beanToMap(restaurant);
                    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
                    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
                    Map<byte[], byte[]> restaurantStringMap = Maps.newHashMap();
                    restaurantMap.forEach((k, v) -> {
                        restaurantStringMap.put(stringRedisSerializer.serialize(k), jackson2JsonRedisSerializer.serialize(v));
                    });
                    connection.hMSet(stringRedisSerializer.serialize(key), restaurantStringMap);
                } catch (Exception e) {
                    log.info(restaurant.toString());
                    continue;
                }
            }
            return null;
        });
        long end = System.currentTimeMillis();
        log.info("执行时间:{}", end - start); // 执行时间:22629
    }
}

3. 增量添加

当后台系统审核新餐厅通过以后,将数据写入Redis中,核心代码如下:

Map<String, Object> restaurantMap = BeanUtil.beanToMap(restaurant);
String key = RedisKeyConstant.restaurants.getKey() + restaurant.getId();
redisTemplate.opsForHash().putAll(key, restaurantMap);

6. 更新餐厅缓存

后台修改餐厅信息时(审核通过后),要进行修改,核心代码如下:

// 根据ID信息,查询餐厅信息

// 审核

// 审核通过
// 更新缓存
Map<String, Object> restaurantMap = BeanUtil.beanToMap(restaurant);
String key = RedisKeyConstant.restaurants.getKey() + restaurant.getId();
redisTemplate.opsForHash().putAll(key, restaurantMap);

当客户端食客给餐厅投票时(喜欢/不喜欢餐厅),要记得修改餐厅的likeVotes或dislikeVotes字段,跟修改餐厅不一样的地方就是,只需要修改其中likeVotes和dislikeVotes属性,不需要整体进行修改:

String key = RedisKeyConstant.restaurants.getKey() + restaurant.getId();
redisTemplate.opsForHash().increment(key, "likeVotes", 1);
redisTemplate.opsForHash().increment(key, "dislikeVotes", 1);

当用户查询餐厅时,如果餐厅没有,会查询数据库,然后在更新缓存。

7. 读取餐厅信息

1. RestaurantController

@RestController
public class RestaurantController {

    @Resource
    private RestaurantService restaurantService;
    @Resource
    private HttpServletRequest request;

    /**
     * 根据餐厅 ID 查询餐厅数据
     */
    @GetMapping("{restaurantId}")
    public ResultInfo<Restaurant> findById(@PathVariable Integer restaurantId) {
        Restaurant restaurant = restaurantService.findById(restaurantId);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), restaurant);
    }
}

2. RestaurantService

@Service
@Slf4j
public class RestaurantService {

    @Resource
    public RestaurantMapper restaurantMapper;
    @Resource
    public RedisTemplate redisTemplate;

    /**
     * 根据餐厅 ID 查询餐厅数据
     */
    public Restaurant findById(Integer restaurantId) {
        // 请选择餐厅
        AssertUtil.isTrue(restaurantId == null, "请选择餐厅查看");
        // 获取 Key
        String key = RedisKeyConstant.restaurants.getKey() + restaurantId;
        // 获取餐厅缓存
        LinkedHashMap restaurantMap = (LinkedHashMap) redisTemplate.opsForHash().entries(key);
        // 如果缓存不存在,查询数据库
        Restaurant restaurant = null;
        if (restaurantMap == null || restaurantMap.isEmpty()) {
            log.info("缓存失效了,查询数据库:{}", restaurantId);
            // 查询数据库
            restaurant = restaurantMapper.findById(restaurantId);
            if (restaurant != null) {
                // 数据库中有,更新缓存
                redisTemplate.opsForHash().putAll(key, BeanUtil.beanToMap(restaurant));
            } else {
                // 数据库中没有,写入缓存一个空数据,设置一个失效时间,60s
            }
        } else {
            // 从缓存中查询到了,直接返回
            restaurant = BeanUtil.fillBeanWithMap(restaurantMap, new Restaurant(),false);
        }
        return restaurant;
    }
}

3. RestaurantMapper

public interface RestaurantMapper {

    // 根据餐厅 ID 查询餐厅信息
    @Select("select id, name, cnName, x, y, location, cnLocation, area, telephone, " +
            "email, website, cuisine, average_price, introduction, thumbnail, like_votes," +
            "dislike_votes, city_id, is_valid, create_date, update_date" +
            " from t_restaurants where id = #{id}")
    Restaurant findById(@Param("id") Integer id);
}

4. 网关 ms-gateway 配置

server:
  port: 80

spring:
  application:
    name: ms-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启配置注册中心进行路由功能
          lower-case-service-id: true # 将服务名称转小写
      routes:
        - id: ms-restaurants
          uri: lb://ms-restaurants
          predicates:
            - Path=/restaurants/**
          filters:
            - StripPrefix=1
          
secure:
  ignore:
    urls: # 配置白名单路径
      - /restaurants/**

5. 启动项目测试

http://localhost/restaurants/1
在这里插入图片描述

8. 缓存异常解决方案

这三个问题,说句实在话,一般中小型传统软件企业,很难碰到这个问题。如果有大并发的项目,流量有几百万左右。这三个问题一定要深刻考虑。

1. 缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。做类电商项目的时候,把这货就称为“爆款”。

解决方案:

其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。所以,对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。

(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。

2. 缓存穿透

缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存,就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库,导致所有的请求都怼到数据库上,从而数据库连接异常。

解决方案:

(1) 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。(不推荐)

(2) 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

(3) 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。

(4) 如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。

3. 缓存雪崩

缓存雪崩,是指缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

产生雪崩的原因之一,比如商城马上就要到双十一零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案:

做电商项目的时候,一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

(1) 给缓存的失效时间,加上一个随机值,避免集体失效。

(2) 使用互斥锁,但是该方案吞吐量明显下降了。

(3) 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点:

a. 从缓存A读数据库,有则直接返回

b. A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。

c. 更新线程同时更新缓存A和缓存B

9. 缓存淘汰

1. 最大内存参数

我们的redis数据库的最大缓存、主键失效、淘汰机制等参数都是通过配置文件来配置的。这个文件是我们的redis.config文件。

maxmemory :设置最大内存;

image-20201122184034973.png

2. 内存淘汰策略

截至目前 redis 一共为我们提供了八个不同的内存置换策略。很早之前提供了6种。

image-20201122183955375.png

(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。

(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。

(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。

(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。

(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰

(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰

(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。

3. 淘汰机制的实现

既然是淘汰,那就需要把这些数据给删除,然后保存新的。Redis 删除策略主要有以下几种:

**惰性删除:**master 节点每次读取命令时都会检查键是否超时,如果超时则执行 del 命令删除键对象,之后异步把 del 命令 slave 节点,这样可以保证数据复制的一致性,slave 节点是永远不会主动去删除超时数据的。

**定时删除:**Redis 的 master 节点在内部定时任务,会循环采样一定数量的键,当发现采样的键过期时,会执行 del 命令,之后再同步个 slave 节点。

主动删除:当前已用内存超过 maxMemory 限定时,触发主动清理策略。主动设置的前提是设置了maxMemory 的值。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我一直在流浪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值