WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
  • MySQL语法文档
  • Redis笔记文档
  • mysql笔记文档

1. Redis

Redis是一款基于内存的、使用K-V结构来实现读写数据的NoSQL非关系型数据库

  • 基于内存的:Redis访问的数据都在内存中

    • Redis的读写效率非常高
    • 其实Redis也会自动的处理持久化,但是正常读写都是在内存中执行的
  • NoSQL:不涉及SQL语句,No可理解为日常英语中的no(没有),也可理解为No Operation(不操作)

  • 非关系型数据库:不关心数据库中存储的是什么数据,几乎没有数据种类的概念,更不存在数据与数据之间的关联

通常,在项目中,Redis用于实现缓存!

image-20221111143809078

当使用Redis后:

  • 【优点】读取数据的效率会高很多
  • 【优点】能够一定程度上保障缓解数据库的查询压力,提高数据库的安全性
  • 【缺点】需要关注数据一致性问题,即Redis中的数据与MySQL中的数据是否一致,如果不一致,是否需要处理

2. Redis的基本操作

在Windows操作系统中,通过.msi安装包来安装的Redis,会自动注册Redis服务,开机会自动启动Redis,所以,Redis处于随时可用的状态。

可以在命令提示符窗口或终端窗口通过redis-cli命令,登录Redis控制台:

D:\IdeaProjects\jsd2207-csmall-product-teacher>redis-cli
127.0.0.1:6379>

当操作提示符变成 127.0.0.1:6379> 后,表示已经登录到Redis客户端的控制台。

在Redis客户端控制台中,可以通过ping命令实时检测Redis是否仍处理可用状态,如果Redis服务正常可用,将反馈PONG:

127.0.0.1:6379> ping
PONG

在Redis客户端控制台中,可以通过exit命令退出,以回到操作系统的终端:

127.0.0.1:6379> exit

D:\IdeaProjects\jsd2207-csmall-product-teacher>

在Redis客户端控制台中,可以通过set命令向Redis中存入值数据,例如:

127.0.0.1:6379> set name wangkejing
OK

在Redis客户端控制台中,可以通过get命令向Redis中存入值数据,例如:

127.0.0.1:6379> get name
"wangkejing"
127.0.0.1:6379> get email
(nil)

提示:以上使用的set命令,既是新增数据的命令(当Key尚且不存在时),也是修改数据的命令(当Key已经存在时),例如:

127.0.0.1:6379> set name fanchuanqi
OK
127.0.0.1:6379> get name
"fanchuanqi"

在Redis客户端控制台中,可以通过keys命令向Redis中存入值数据,此命令必须有参数,在参数中可以使用通配符,例如:

127.0.0.1:6379> keys username1
1) "username1"

127.0.0.1:6379> keys username0
(empty list or set)

127.0.0.1:6379> keys user*
1) "username1"
2) "username3"
3) "username2"
4) "username4"

127.0.0.1:6379> keys *
1) "username1"
2) "email1"
3) "email2"
4) "username3"
5) "name"
6) "username2"
7) "username4"

注意:keys命令会根据模式查找当前Redis中的Key,可能耗时较长,会导致“阻塞”,所以,在生产环境中,一般不允许使用此命令!

在Redis客户端控制台中,可以通过dbsize命令查看Redis中的数据的数量,例如:

127.0.0.1:6379> dbsize
(integer) 7

在Redis客户端控制台中,可以通过del命令删除Redis中指定Key的数据,例如:

127.0.0.1:6379> del username1
(integer) 1

在Redis客户端控制台中,可以通过flushall命令删除Redis中的数据,例如:

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)

更多命令,可查阅资料,例如:https://blog.csdn.net/weixin_46742102/article/details/109483603

Redis中的传统数据类型有:string、list、hash、set、zset。

3. Redis编程

需要添加依赖项:

<!-- Spring Boot Data Redis的依赖项,用于实现Redis编程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在Spring系列框架中,Redis编程需要使用RedisTemplate工具类,此工具类应该事先创建、配置,并保存到Spring容器中,当需要使用时,自动装配此工具类的对象。

在根包下创建RedisConfiguration配置类,在此类中使用@Bean方法来配置RedisTemplate:

package cn.tedu.csmall.product.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.io.Serializable;

/**
 * Redis配置类
 *
 * @author java@tedu.cn
 * @version 0.0.1
 
 */
@Slf4j
@Configuration
public class RedisConfiguration {

    public RedisConfiguration() {
        log.debug("创建配置类对象:RedisConfiguration");
    }

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());
        return redisTemplate;
    }

}

关于Redis中的list类型的数据,它是一种先进后出、后进先出的栈结构:

image-20221111174239963

在Redis中操作list时,允许从左侧或右侧进行操作(请将此栈结构想像为横着的):

image-20221111174343596

image-20221111174416595

image-20221114093420529

关于Redis读写数据,常用API大致如下:

package cn.tedu.csmall.product;

import cn.tedu.csmall.product.pojo.vo.AlbumStandardVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Slf4j
@SpringBootTest
public class RedisTests {

    @Autowired
    RedisTemplate<String, Serializable> redisTemplate;

    // Redis编程相关API
    // =====================
    // 【RedisTemplate类】
    // ValueOperations opsForValue() >>> 获取ValueOperations对象,操作Redis中的string类型时需要此对象
    // ListOperations opsForList() >>> 获取ListOperations对象,操作Redis中的list类型时需要此对象
    // Set<String> keys(String pattern) >>> 根据模式pattern搜索Key
    // Boolean delete(String key) >>> 根据Key删除数据,返回成功与否
    // Long delete(Collection<String> keys) >>> 根据Key的集合批量删除数据,返回成功删除的数据的数量
    //
    // 【ValueOperations类】
    // void set(String key, Serializable value) >>> 向Redis中写入数据
    // Serializable get(String key) >>> 读取Redis中的数据

    @Test
    void valueSet() {
        String key = "username1";
        String value = "王克晶";
		//添加数据
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        ops.set(key, value);
    }

    @Test
    void valueSetObject() {
        String key = "album2022";
        AlbumStandardVO album = new AlbumStandardVO();
        album.setId(2022L);
        album.setName("测试相册00001");
        album.setDescription("测试简介00001");
        album.setSort(998);
		//添加对象数据
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        ops.set(key, album);
    }

    @Test
    void valueGet() {
        String key = "username1";
	    //取出数据
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        Serializable value = ops.get(key);
        log.debug("从Redis中取出Key值为【{}】的数据,结果:{}", key, value);
    }

    @Test
    void valueGetObject() {
        String key = "album2022";
		//取出对象数据
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        Serializable value = ops.get(key);
        log.debug("从Redis中取出Key值为【{}】的数据", key);
        log.debug("结果:{}", value);
        log.debug("结果的数据类型:{}", value.getClass().getName());
    }

    @Test
    void keys() {
        String pattern = "*";
		//根据字符串搜索key
        Set<String> keys = redisTemplate.keys(pattern);
        log.debug("根据模式【{}】搜索Key,结果:{}", pattern, keys);
    }

    @Test
    void delete() {
        String key = "username1";
		//根据key删除数据
        Boolean result = redisTemplate.delete(key);
        log.debug("根据Key【{}】删除数据完成,结果:{}", key, result);
    }

    @Test
    void deleteX() {
        Set<String> keys = new HashSet<>();
        keys.add("username2");
        keys.add("username3");
        keys.add("username4");
		//根据key集合删除数据
        Long count = redisTemplate.delete(keys);
        log.debug("根据Key集合【{}】删除数据完成,成功删除的数据的数量:{}", keys, count);
    }

    @Test
    void rightPush() {
        String key = "stringList";
        List<String> stringList = new ArrayList<>();
        for (int i = 1; i <= 8; i++) {
            stringList.add("string-" + i);
        }
		//右压列表数据
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        for (String s : stringList) {
            ops.rightPush(key, s);
        }
    }

    @Test
    void size() {
        String key = "stringList";
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        Long size = ops.size(key);
        //根据Key读取列表的长度
        log.debug("根据Key【{}】读取列表的长度,结果:{}", key, size);
    }

    @Test
    void range() {
        String key = "stringList";
        long start = 0;
        long end = -1;
//根据Key从0到1读取列表,结果长度
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        List<Serializable> list = ops.range(key, start, end);
        log.debug("根据Key【{}】从【{}】到【{}】读取列表,结果长度:{}", key, start, end, list.size());
        for (Serializable item : list) {
            log.debug("列表项:{}", item);
        }
    }

}

4. 关于数据一致性问题的思考

当使用Redis缓存数据后,如果数据库中的数据发生了变化,此时,Redis中的数据会暂时与MySQL数据库中的数据并不相同,通常称之为“数据一致性”问题!对于此问题,只可能:

  • 及时更新Redis缓存数据,使得Redis中的数据与MySQL数据库中的数据是一致的
  • 放任数据不一致的表现,即Redis中的数据在接下来的一段时间里是不准确的,直至更新Redis中的数据,才会是准确的数据

如果及时更新Redis缓存数据,其优点是Redis缓存中的数据基本上是准确的,其缺点在于可能需要频繁的更新Redis缓存数据,本质上反复读MySQL、反复写Redis的操作,如果读取Redis数据的频率根本不高,则会形成浪费,并且,更新缓存的频率太高,也会增加服务器的压力,所以,对于增、删、改频率非常高的数据,可能不太适用此规则!

放任数据不一致的表现,其缺点很显然就是数据可能不准确,但是,其优点是没有给服务器端增加任何压力,需要注意:其实,并不是所有数据都必须时时刻刻都要求准确性的,某些数据即使不准确,也不会产生恶劣后果(例如热门话题排行榜,定期更新即可),或者,某些数据即使准确,也没有太多实际意义(例如热门时段的火车票、飞机票等,即使在列表中显示了正确的余量,也不一定能够成功购买)。

基于使用Redis缓存数据可能存在数据一致性问题,通常,使用Redis缓存的数据可能:

  • 数据的增、删、改的频率非常低,查询频率相对更高(甚至非常高)
    • 例如电商平台中的商品类别、品牌
    • 无论采取即时更新Redis的策略,还是定期更新Redis的策略,都是可行的解决方案
  • 对数据的准确性要求不高的数据
    • 例如某些列表或榜单
    • 通常使用定期更新Redis的策略,更新周期应该根据数据变化的频率及其价值来决定

4.1. 缓存品牌数据

通常,推荐将Redis的读写数据操作进行封装,则先在根包下创建repo.IBrandRedisRepository接口:

public interface IBrandRedisRepository {}

然后,在根包下repo.impl.BrandRedisRepositoryImpl创建以上接口的实现类:

@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {
    public BrandRedisRepositoryImpl() {
        log.debug("创建处理缓存的数据访问对象:BrandRedisRepositoryImpl");
    }
}

接下来,应该在接口中添加读写数据的抽象方法,并在实现类中实现这些方法!

在IBrandRedisRepository接口中添加抽象方法:

package cn.tedu.csmall.product.repo;

import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;

import java.util.List;

/**
 * 处理品牌缓存的数据访问接口
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
public interface IBrandRedisRepository {
    
    /**
     * 品牌数据项在Redis中的Key前缀
     */
    String BRAND_ITEM_KEY_PREFIX = "brand:item:";
    /**
     * 品牌列表在Redis中的Key
     */
    String BRAND_LIST_KEY = "brand:list";
    /**
     * 所有品牌数据项的Key
     */
    String BRAND_ITEM_KEYS_KEY = "brand:item-keys";

    /**
     * 向Redis中写入品牌数据
     *
     * @param brandStandardVO 品牌数据
     */
    void save(BrandStandardVO brandStandardVO);

    /**
     * 向Redis中写入品牌列表
     *
     * @param brands 品牌列表
     */
    void save(List<BrandListItemVO> brands);

    /**
     * 删除Redis中全部品牌数据,包括各品牌详情数据和品牌列表等
     *
     * @return 成功删除的数据的数量
     */
    Long deleteAll();

    /**
     * 从Redis中读取品牌数据
     *
     * @param id 品牌id
     * @return 匹配的品牌数据,如果没有匹配的数据,则返回null
     */
    BrandStandardVO get(Long id);

    /**
     * 从Redis中读取品牌列表
     *
     * @return 品牌列表
     */
    List<BrandListItemVO> list();

    /**
     * 从Redis中读取品牌列表
     *
     * @param start 读取数据的起始下标
     * @param end   读取数据的截止下标
     * @return 品牌列表
     */
    List<BrandListItemVO> list(long start, long end);

}

并在BrandRedisRepositoryImpl中实现此方法:

package cn.tedu.csmall.product.repo.impl;

import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import cn.tedu.csmall.product.repo.IBrandRedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 处理品牌缓存的数据访问实现类
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    public BrandRedisRepositoryImpl() {
        log.debug("创建处理缓存的数据访问对象:BrandRedisRepositoryImpl");
    }

    @Override
    public void save(BrandStandardVO brandStandardVO) {
        String key = BRAND_ITEM_KEY_PREFIX + brandStandardVO.getId();
        redisTemplate.opsForSet().add(BRAND_ITEM_KEYS_KEY, key);
        redisTemplate.opsForValue().set(key, brandStandardVO);
    }

    @Override
    public void save(List<BrandListItemVO> brands) {
        String key = BRAND_LIST_KEY;
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        for (BrandListItemVO brand : brands) {
            ops.rightPush(key, brand);
        }
    }

    @Override
    public Long deleteAll() {
        // 获取到所有item的key
        Set<Serializable> members = redisTemplate
                .opsForSet().members(BRAND_ITEM_KEYS_KEY);
        Set<String> keys = new HashSet<>();
        for (Serializable member : members) {
            keys.add((String) member);
        }
        // 将List和保存Key的Set的Key也添加到集合中
        keys.add(BRAND_LIST_KEY);
        keys.add(BRAND_ITEM_KEYS_KEY);
        return redisTemplate.delete(keys);
    }

    @Override
    public BrandStandardVO get(Long id) {
        Serializable serializable = redisTemplate
                .opsForValue().get(BRAND_ITEM_KEY_PREFIX + id);
        BrandStandardVO brandStandardVO = null;
        if (serializable != null) {
            if (serializable instanceof BrandStandardVO) {
                brandStandardVO = (BrandStandardVO) serializable;
            }
        }
        return brandStandardVO;
    }

    @Override
    public List<BrandListItemVO> list() {
        long start = 0;
        long end = -1;
        return list(start, end);
    }

    @Override
    public List<BrandListItemVO> list(long start, long end) {
        String key = BRAND_LIST_KEY;
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        List<Serializable> list = ops.range(key, start, end);
        List<BrandListItemVO> brands = new ArrayList<>();
        for (Serializable item : list) {
            brands.add((BrandListItemVO) item);
        }
        return brands;
    }

}

完成后,可以调整“查询品牌列表”的业务,将原本从数据库中查询数据改为从Redis缓存中查询数据,则修改BrandServiceImpl中的list()方法:

@Override
public List<BrandListItemVO> list() {
    log.debug("开始处理【查询品牌列表】的业务,无参数");
    // return brandMapper.list();
    return brandRedisRepository.list();
}

至于,查询品牌列表时将从Redis中查询数据。

4.2. 缓存预热

当启用项目时就将缓存数据加载到Redis缓存中,这种做法通常称之为“缓存预热”。

在Spring Boot项目中,自定义组件类,实现ApplicationRunner接口,此接口中的run()方法将在启动项目之后自动执行,可以通过此机制实现缓存预热。

在根包下创建preload.CachePreload类并实现缓存预热:

package cn.tedu.csmall.product.preload;

import cn.tedu.csmall.product.mapper.BrandMapper;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.repo.IBrandRedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class CachePreload implements ApplicationRunner {

    @Autowired
    private BrandMapper brandMapper;
    @Autowired
    private IBrandRedisRepository brandRedisRepository;

    public CachePreload() {
        log.debug("创建开机自动执行的组件对象:CachePreload");
    }

    // ApplicationRunner中的run()方法会在项目启动成功之后自动执行
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.debug("CachePreload.run()");

        log.debug("准备删除Redis缓存中的品牌数据……");
        brandRedisRepository.deleteAll();
        log.debug("删除Redis缓存中的品牌数据,完成!");

        log.debug("准备从数据库中读取品牌列表……");
        List<BrandListItemVO> list = brandMapper.list();
        log.debug("从数据库中读取品牌列表,完成!");

        log.debug("准备将品牌列表写入到Redis缓存……");
        brandRedisRepository.save(list);
        log.debug("将品牌列表写入到Redis缓存,完成!");
    }

}

4.3. 手动更新缓存

由于在业务逻辑层已经实现“重建品牌缓存”的功能,在控制器中添加处理请求的方法,即可实现手动更新缓存:

// http://localhost:9080/brands/cache/rebuild
@ApiOperation("重建品牌缓存")
@ApiOperationSupport(order = 600)
@PostMapping("/cache/rebuild")
public JsonResult<Void> rebuildCache() {
    log.debug("开始处理【重建品牌缓存】的请求,无参数");
    brandService.rebuildCache();
    return JsonResult.ok();
}

后续,客户端只需要提交请求,即可实现“重建品牌缓存”。

4.4. 按需加载缓存数据

假设当根据id获取品牌详情时,需要通过“按需加载缓存数据”的机制来实现缓存,可以将原业务调整为:

@Override
public BrandStandardVO getStandardById(Long id) {
    log.debug("开始处理【根据id查询品牌详情】的业务,参数:{}", id);
    // 根据id从缓存中获取数据
    log.debug("将从Redis中获取相关数据");
    BrandStandardVO brand = brandRedisRepository.get(id);
    // 判断获取到的结果是否不为null
    if (brand != null) {
        // 是:直接返回
        log.debug("命中缓存,即将返回:{}", brand);
        return brand;
    }

    // 无缓存数据,从数据库中查找数据
    log.debug("未命中缓存,即将从数据库中查找数据");
    brand = brandMapper.getStandardById(id);
    // 判断查询到的结果是否为null
    if (brand == null) {
        // 是:抛出异常
        String message = "获取品牌详情失败,尝试访问的数据不存在!";
        log.warn(message);
        throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
    }

    // 将查询结果写入到缓存,并返回
    log.debug("从数据库查询到有效结果,将查询结果存入到Redis:{}", brand);
    brandRedisRepository.save(brand);
    log.debug("返回结果:{}", brand);
    return brand;
}

5.Redis 强化

5.1.缓存使用原则

什么时候,什么样的数据能够保存在Redis中?

1.数据量不能太大

2.使用越频繁,Redis保存这个数据越值得

3.保存在Redis中的数据一般不会是数据库中频繁修改的

5.2.缓存淘汰策略

Redis将数据保存在内存中, 内存的容量是有限的

如果Redis服务器的内存已经全满,现在还需要向Redis中保存新的数据,如何操作,就是缓存淘汰策略

  • noeviction:返回错误**(默认)**

如果我们不想让它发生错误,就可以设置它将满足某些条件的信息删除后,再将新的信息保存

  • allkeys-random:所有数据中随机删除数据

  • volatile-random:有过期时间的数据中随机删除数据

  • volatile-ttl:删除剩余有效时间最少的数据

    科学↓

  • allkeys-lru:所有数据中删除上次使用时间距离现在最久的数据

  • volatile-lru:有过期时间的数据中删除上次使用时间距离现在最久的数据

  • allkeys-lfu:所有数据中删除使用频率最少的

  • volatile-lfu:有过期时间的数据中删除使用频率最少的

Time To Live (ttl)

Least Recently Used (lru)

Least Frequently Used (lfu)

5.3.缓存穿透

所谓缓存穿透,就是一个业务请求先查询redis,redis没有这个数据,那么就去查询数据库,但是数据库也没有的情况

正常业务下,一个请求查询到数据后,我们可以将这个数据保存在Redis

之后的请求都可以直接从Redis查询,就不需要再连接数据库了

但是一旦发生上面的穿透现象,仍然需要连接数据库,一旦连接数据库,项目的整体效率就会被影响

如果有恶意的请求,高并发的访问数据库中不存在的数据,严重的,当前服务器可能出现宕机的情况

解决方案:业界主流解决方案:布隆过滤器

布隆过滤器的使用步骤

1.针对现有所有数据,生成布隆过滤器,保存在Redis中

2.在业务逻辑层,判断Redis之前先检查这个id是否在布隆过滤器中

3.如果布隆过滤器判断这个id不存在,直接返回

4.如果布隆过滤器判断id存在,在进行后面业务执行

5.4.缓存击穿

一个计划在Redis保存的数据,业务查询,查询到的数据Redis中没有,但是数据库中有

这种情况要从数据库中查询后再保存到Redis,这就是缓存击穿

但是这个情况也不是异常情况,因为我们大多数数据都需要设置过期时间,而过期时间到时,这个数据就会从Redis中移除,再有请求查询这个数据,就一定会从数据库中再次同步

缓存击穿本身并不是灾难性的问题,也不是不允许发生的现象

5.5.缓存雪崩

上面讲到击穿现象

同一时间发生少量击穿是正常的

但是如果出现同一时间大量击穿现象就会如下图

所谓缓存雪崩,指的就是Redis中保存的数据,短时间内有大量数据同时到期的情况

如上图所示,本应该由Redis反馈的信息,由于雪崩都去访问了Mysql,mysql承担不了,非常可能导致异常

要想避免这种情况,就需要避免大量缓存同时失效

大量缓存同时失效的原因:通常是同时加载的数据设置了相同的有效期导致的

我们可以通过在设置有效期时添加一个随机数,这样就能够防止大量数据同时失效了

5.6.Redis持久化

Redis将信息保存在内存

内存的特征就是一旦断电,所有信息都丢失,对于Redis来讲,所有数据丢失后,再重新加载数据,就需要从数据库重新查询所有数据,这个操作不但耗费时间,而且对数据库的压力也非常大

而且有些业务是先将数据保存在Redis,隔一段时间和数据库同步的

如果Redis断电,这段时间的数据就完全丢失了!

为了防止Redis的重启对数据库带来额外的压力和数据的丢失,Redis支持了持久化的功能

所谓持久化就是将Redis中保存的数据,以指定方式保存在Redis当前服务器的硬盘上

如果存在硬盘上,那么断电数据也不会丢失,再启动Redis时,利用硬盘中的信息来回复数据

Redis实现持久化有两种策略

RDB:(Redis Database Backup)

RDB本质上就是数据库快照(就是当前Redis中所有数据转换成二进制的对象,保存在硬盘上)

默认情况下,每次备份会生成一个dump.rdb的文件

当Redis断电或宕机后,重新启动时,会从这个文件中恢复数据,获得dump.rdb中所有内容

实现这个效果我们可以在Redis的配置文件中添加如下信息

save 60 5

上面配置中60表示秒

5表示Redis的key被更新的次数

配置效果:1分钟内如果有5个及以上的key被更新,就启动rdb数据库快照程序

优点:

  • 因为是整体Redis数据的二进制格式,数据恢复是整体恢复的

缺点:

  • 生成的rdb文件是一个硬盘上的文件,读写效率是较低的
  • 如果突然断电,只能恢复到最后一次生成的rdb中的数据

AOF(Append Only File):

AOF策略是将Redis运行过的所有命令(日志)备份下来,保存在硬盘上

这样即使Redis断电,我们也可以根据运行过的日志,恢复为断电前的样子

我们可以在Redis的配置文件中添加如下配置信息

appendonly yes

经过这个设置,就能保存运行过的指令的日志了

理论上任何运行过的指令都可以恢复

但是实际情况下,Redis非常繁忙时,我们会将日志命令缓存之后,整体发送给备份,减少io次数以提高备份的性能 和对Redis性能的影响

实际开发中,配置一般会采用每秒将日志文件发送一次的策略,断电最多丢失1秒数据

优点:

​ 相对RDB来讲,信息丢失的较少

缺点:

​ 因为保存的是运行的日志,所以占用空间较大

实际开发中RDB和AOF是可以同时开启的,也可以选择性开启

Redis的AOF为减少日志文件的大小,支持AOF rewrite

简单来说就是将日志中无效的语句删除,能够减少占用的空间

5.7.Redis存储原理

我们在编写java代码业务时,如果需要从多个元素的集合中寻找某个元素取出,或检查某个Key在不在的时候,推荐我们使用HashMap或HashSet,因为这种数据结构的查询效率最高,因为它内部使用了

"散列表"

下图就是散列表的存储原理

image-20221206155432504

槽位越多代表元素多的时候,查询性能越高,HashMap默认16个槽

Redis底层保存数据用的也是这样的散列表的结构

Redis将内存划分为16384个区域(类似hash槽)

将数据的key使用CRC16算法计算出一个值,取余16384

得到的结果是0~16383

这样Redis就能非常高效的查找元素了

5.8.Redis集群

Redis最小状态是一台服务器

这个服务器的运行状态,直接决定Redis是否可用

如果它离线了,整个项目就会无Redis可用

系统会面临崩溃

为了防止这种情况的发生,我们可以准备一台备用机

主从复制

1657014182997

也就是主机(master)工作时,安排一台备用机(slave)实时同步数据,万一主机宕机,我们可以切换到备机运行

缺点,这样的方案,slave节点没有任何实质作用,只要master不宕机它就和没有一样,没有体现价值

读写分离

1657014449976

这样slave在master正常工作时也能分担Master的工作了

但是如果master宕机,实际上主备机的切换,实际上还是需要人工介入的,这还是需要时间的

那么如果想实现发生故障时自动切换,一定是有配置好的固定策略的

哨兵模式:故障自动切换

1657014722404

哨兵节点每隔固定时间向所有节点发送请求

如果正常响应认为该节点正常

如果没有响应,认为该节点出现问题,哨兵能自动切换主备机

如果主机master下线,自动切换到备机运行

1657014957753

但是如果哨兵判断节点状态时发生了误判,那么就会错误将master下线,降低整体运行性能

所以要减少哨兵误判的可能性

哨兵集群

1657071387427

我们可以将哨兵节点做成集群,由多个哨兵投票决定是否下线某一个节点

哨兵集群中,每个节点都会定时向master和slave发送ping请求

如果ping请求有2个(集群的半数节点)以上的哨兵节点没有收到正常响应,会认为该节点下线

当业务不断扩展,并发不断增高时

分片集群

只有一个节点支持写操作无法满足整体性能要求时,系统性能就会到达瓶颈

这时我们就要部署多个支持写操作的节点,进行分片,来提高程序整体性能

分片就是每个节点负责不同的区域

Redis0~16383号槽,

例如

MasterA负责0~5000

MasterB负责5001~10000

MasterC负责10001~16383

一个key根据CRC16算法只能得到固定的结果,一定在指定的服务器上找到数据

1657072179480

有了这个集群结构,我们就能更加稳定和更加高效的处理业务请求了

为了节省哨兵服务器的成本,有些公司在Redis集群中直接添加哨兵功能,既master/slave节点完成数据读写任务的同时也都互相检测它们的健康状态

有额外精力的同学,可以自己查询Redis分布式锁的解决方案(redission)

最近更新:: 2025/8/22 15:05
Contributors: yanpeng_
Prev
MySQL语法文档
Next
mysql笔记文档