Redis

Redis

关于Redis

Redis是一种基于内存的,使用K-V结构存储数据的NoSQL非关系型数据库。

提示:Redis也会占用磁盘空间,并自动将数据同步到磁盘中,所以,存入到Redis中的数据,即使重启电脑,再次开机时,Redis中仍有此前存入的数据。但是,Redis在读写过程中仍是基于内存的。

Redis的主要作用是缓存数据,通常,会将关系型数据库(例如MySQL等)中的数据读取出来,并写入到Redis中,后续,当需要获取数据时,将优先从Redis中获取,而不是从关系型数据库中获取!

由于Redis是基于内存的,读写效率远高于基于磁盘存储数据的关系型数据库,同时,Redis相比关系型数据库来说,单次查询耗时更短,所以,可以承受更加的查询访问量,并减少对关系型数据库的访问,可以起到“保护”关系型数据库的作用!

Redis的数据类型

Redis中的经典数据类型有5种:string / list / set / hash / z-set

  • 在Java语言中的简单数据类型,在Redis中对应的都是string类型

另外,还有:bitmap / hyperloglog / Geo / 流

Redis的常用命令

当登录Redis的客户端后(命令提示符变成127.0.0.1:6379>状态后),可以:

  • set KEY VALUE:存入数据,例如:set username root,如果反复使用同一个KEY执行此命令,后续存入的VALUE会覆盖前序存入的VALUE,相当于“修改数据”,如果使用的是此前从未使用过的KEY,则会新增数据

  • get KEY:取出数据,例如:get username,如果KEY存在,则取出对应的数据,如果KEY不存在,则返回(nil),相当于Java中的null

  • keys PATTERN:根据模式(PATTERN)获取KEY,例如:keys username,如果KEY存在,则返回,如果不存在,则返回(empty list or set),在PATTERN处可以使用星号(*)作为通配符,例如:keys username*可以返回当前Redis中所有以username作为前缀的KEY,返回的多个KEY在显示时是无序的,甚至,还可以使用keys *查询当前Redis中所有KEY

  • **注意:**在生产环境中,禁止使用此命令

  • del KEY [KEY ...]:删除指定KEY对应的数据,例如:del username,将返回删除了多少条数据

  • flushdb:清空当前数据库

更多命令可参考:https://www.cnblogs.com/antLaddie/p/15362191.html

Redis中的List类型数据

在Redis中,List类型的数据是一个先进后出、后进先出的栈结构:

在学习Redis时,你应该把Redis中的List相像成一个在以上图示的基础上旋转了90度的栈!

在Redis中的List,可以从左侧进行压栈操作,例如:

也可以从右侧进行压栈操作,例如:

并且,从Redis中读取List数据时,都是从左至右读取,通常,为了更加符合平时使用列表的习惯,大多情况下会采取“从右侧压入数据”。

注意:在Redis中的List数据,每个元素都同时拥有2个下标,一个是从左至右、从0开始递增编号的,另一个是从右至左、从-1开始递减编号的!

后续,在读取List中的区间时,end表示的元素不可以是相对start更靠左的元素!

同时,-1始终是最后一个元素的下标,所以,当你需要读取整个列表的数据时,start为0,end为-1。

Redis编程

在Spring Boot项目中,实现Redis编程需要添加依赖项:

<!-- Spring Boot支持Redis编程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在读写Redis中的数据时,主要使用RedisTemplate工具类的对象,通常,会使用配置类中的@Bean方法来配置这个类的对象,以便于需要读写Redis时可以直接自动装配此对象。

在项目的根包下创建config.RedisConfiguration类,并配置:

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;
    }

}

关于string、List、Set类型数据的基本访问:

@Slf4j
@SpringBootTest
public class RedisTest {

    // 提示:当写入的数据包含“非ASCII码字符”时,在终端窗口中无法正常显示,是正常现象,也并不影响后续读取数据
    // opsForValue():返回ValueOperations对象,只要是对Redis中的string进行操作,都需要此类型对象的API
    // opsForList():返回ListOperations对象,只要是对Redis中的list进行操作,都需要此类型对象的API
    // opsForSet():返回SetOperations对象,只要是对Redis中的set进行操作,都需要此类型对象的API
    @Autowired
    RedisTemplate<String, Serializable> redisTemplate;

    // 存入字符串类型的值
    @Test
    void set() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        opsForValue.set("username", "好好学习");
        log.debug("向Redis中存入Value类型(string)的数据,成功!");
    }

    // 读取字符串类型的值
    @Test
    void get() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        String key = "username";
        Serializable value = opsForValue.get(key);
        log.debug("从Redis中读取Value类型(string)数据,Key={},Value={}", key, value);
    }

    // 可以存入对象
    @Test
    void setObject() {
        Brand brand = new Brand();
        brand.setId(1L);
        brand.setName("大米");

        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        opsForValue.set("brand1", brand);
        log.debug("向Redis中存入Value类型(string)的数据,成功!");
    }

    // 读取到的对象的类型就是此前存入时的类型
    @Test
    void getObject() {
        try {
            ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
            String key = "brand1";
            Serializable value = opsForValue.get(key);
            log.debug("从Redis中读取Value类型(string)数据,完成!");
            log.debug("Key={},Value={}", key, value);

            log.debug("Value的类型:{}", value.getClass().getName());
            Brand brand = (Brand) value;
            log.debug("可以将Value转换回此前存入时的类型!");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    // 如果Key并不存在,则读取的结果为null
    @Test
    void getEmpty() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        String key = "EmptyKey";
        Serializable value = opsForValue.get(key);
        log.debug("从Redis中读取Value类型(string)数据,Key={},Value={}", key, value);
    }

    // 使用keys命令对应的API
    @Test
    void keys() {
        String pattern = "username*";
        Set<String> keys = redisTemplate.keys(pattern); // keys *
        log.debug("根据模式【{}】查询Key,结果:{}", pattern, keys);
    }

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

    // 批量删除指定的Key的数据,与删除某1个数据的方法相同,只是参数列表不同
    @Test
    void deleteBatch() {
        Set<String> keys = new HashSet<>();
        keys.add("username");
        keys.add("username1");
        keys.add("username2");

        Long count = redisTemplate.delete(keys);
        log.debug("根据Key【{}】批量删除数据,删除的数据的数量:{}", keys, count);
    }

    // 向Redis中存入List类型的数据
    // 注意:反复执行相同的代码,会使得同一个List中有多份同样的数据
    @Test
    void rightPush() {
        List<Album> albumList = new ArrayList<>();
        for (int i = 1; i <= 8; i++) {
            Album album = new Album();
            album.setId(i + 0L);
            album.setName("测试相册-" + i);
            albumList.add(album);
        }

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        String key = "album:lists";
        for (Album album : albumList) {
            opsForList.rightPush(key, album);
        }
        log.debug("向Redis中写入List类型的数据,完成!");
    }

    // 从Redis中取出List类型的数据列表
    @Test
    void range() {
        String key = "albums";
        long start = 0L;
        long end = -1L;

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        List<Serializable> serializableList = opsForList.range(key, start, end);
        log.debug("从Redis中读取Key【{}】的List数据,数据量:{}", key, serializableList.size());
        for (Serializable serializable : serializableList) {
            log.debug("{}", serializable);
        }
    }

    // 从Redis中读取List的长度
    @Test
    void size() {
        String key = "albums";

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        Long size = opsForList.size(key);
        log.debug("从Redis中读取Key【{}】的List的长度,结果:{}", key, size);
    }

    // 向Redis中存入Set类型的数据
    // Set中的元素必须是唯一的,如果反复添加,后续的添加并不会成功
    @Test
    void add() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long add = opsForSet.add(key, "album:item:100");
        log.debug("向Redis中存入Set类型的数据,结果:{}", add);
    }

    // 向Redis中批量存入Set类型的数据
    @Test
    void addBatch() {
        String key = "brandItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long add = opsForSet.add(key, "brand:item:1", "brand:item:2", "brand:item:3");
        log.debug("向Redis中存入Set类型的数据,结果:{}", add);
    }

    // 从Redis中取出Set类型的数据集合
    @Test
    void members() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Set<Serializable> members = opsForSet.members(key);
        log.debug("从Redis中读取Key【{}】的Set数据,数据量:{}", key, members.size());
        for (Serializable serializable : members) {
            log.debug("{}", serializable);
        }
    }

    // 从Redis中读取Set的长度
    @Test
    void sizeSet() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long size = opsForSet.size(key);
        log.debug("从Redis中读取Key【{}】的Set的长度,结果:{}", key, size);
    }

}

关于Key的格式

在绝大多数Redis的可视化工具(例如Another Redis Desktop Manager)中,会自动处理Key中的冒号(:),将多个前缀相同的Key放在相同的“文件夹”中!

其中,冒号(:)是默认的建议的分隔符号,但并不一定必须使用冒号,也可以改为其它符号!

注意:使用冒号作为分隔符,也只是为了更加方便的使用相关的可视化工具,对Redis的数据读写并没有什么影响!

另外,Key的定义,应该是多层级的,并且,应该保证同类的数据一定具有相同的组成部分,不同类的数据一定与其它数据能明确的区分开来!

例如:

  • 每个品牌数据:brand:item:1、category:item:1

  • 品牌列表:brand:list、category:list

使用Redis时的数据一致性问题

在开发实践中,数据最终都是保存在关系型数据库的,例如保存到MySQL中,同时,为了提高查询效率、保护关系型数据库,通常会将某些数据从关系型数据库中读取出来,存入到Redis中,后续,将优先从Redis中读取数据!

由于在关系型数据库中和在Redis中都存入了数据,如果某个数据发生了变化,通常是修改关系型数据库中的数据,当时,如果Redis中并没有及时更新,但仍从Redis中获取数据,获取到的数据就会是不准确的!

所以,当同一个数据存储到了不同的存储位置,就可能出现数据一致性问题,即2个或多不同的存储位置中,“同样”的数据其实并不相同!

关于数据一致性问题:

  • 并不是有必要及时的更新数据,即:当关系型数据库中的数据发生变化后,并不一定需要更新Redis中的数据,此时,Redis中的数据是“不准确”的,但是,对于软件的使用并没有什么影响

  • 例如:购买火车票时,在列表页面中显示的各趟车次的票的余量

  • 某些数据可能更新频率极低,这些数据可能一开始就没有太多的数据一致性问题,并且,可以在关系型数据库中的数据发生变化时,立刻更新Redis中的数据

  • 例如:商品的类别

  • 另外,并不是所有数据都适合放在Redis中的,例如访问频率极低的数据,或者,数据量特别大的数据

  • 例如:用户的历史订单

关于数据一致性问题的常见解决方案:

  • 即时更新:当关系型数据库的数据发生变化后,马上更新Redis中的数据

  • 周期性更新:当关系型数据库的数据发生变化后,不会马上更新Redis中的数据,而是每隔一段时间更新一次

  • 手动更新:当关系型数据库的数据发生变化后,不会马上更新Redis中的数据,而是由管理员明确的操作才会执行更新

使用ApplicationRunner实现缓存预热

在Spring Boot项目中,任何组件类实现ApplicationRunner接口,重写其中的run()方法,此方法会在项目启动之后自动执行。

在项目的根包下创建preload.CachePreload类,在类上添加@Component注解,并实现ApplicationRunner接口,例如:

package cn.tedu.csmall.product.preload;

@Slf4j
@Component
public class CachePreload implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.debug("开始执行CachePreload.run()");
    }

}

启动项目后,可以看到,以上run()方法在项目启动完成之后自动执行了。

计划任务

在Spring Boot项目中,默认是不允许执行任何计划任务的,需要在配置类上添加@EnableScheudling注解来开启。

在Spring Boot项目中,在任何组件类中,在任何方法(公有的、void返回类型的、无参数列表的)上,添加@Scheudled注解,即可使得此方法是计划任务方法。

在@Scheduled注解上,需要配置注解参数来确定计划任务的执行周期或执行时间点。

在项目的根包下创建config.ScheduleConfiguration类,用于启用计划任务:

@Slf4j
@Configuration
@EnableScheduling
public class ScheduleConfiguration {

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

}

然后,在项目的根包下创建schedule.CachSchedule类,作为计划任务类,例如:

@Slf4j
@Component
public class CacheSchedule {

    // fixedRate:执行频率,以毫秒为单位
    @Scheduled(fixedRate = 5 * 1000)
    public void a() {
        log.debug("执行了计划任务……");
    }

}

按照以上配置,计划任务的执行频率就是5秒。

需要注意,通过fixedRate或fixtedDelay配置的计划任务,首次执行是项目启动时就执行了(严格来说,是在项目完成启动的前一刻开始第1次执行),所以,使用这种计划任务时,可以不必再使用ApplicationRunner来处理缓存预热!

缓存使用原则

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

  1. 数据量不能太大

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

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

缓存淘汰策略

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

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

  • noeviction:返回错误(默认)

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

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

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

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

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

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

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

缓存穿透

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

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

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

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

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

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

布隆过滤器的使用步骤

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

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

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

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

缓存击穿

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

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

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

缓存雪崩

上面讲到击穿现象

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

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

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

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

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

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

解决方案:设置仿雪崩随机数

Redis持久化

Redis将信息保存在内存

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

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

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

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

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

那么断电数据也不会丢失,再启动Redis时,利用硬盘中的信息来恢复数据

Redis实现持久化有两种策略

RDB:(Redis Database Backup)

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

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

当Redis断电或宕机后,我们可以在Redis的配置文件中添加如下信息

save 60 s

上面配置中60表示秒

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

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

优点:

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

缺点:

  • 生成的rdb文件是一个硬盘上的文件读写效率是较低的,生成时对Redis的工作性能有影响

  • 如果突然断电只能恢复到最后一次生成的rdb中的数据

AOF(Append Only File)

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

这样即使Redis断电,我们也可以根据运行过的日志,恢复为断电前的样子我们可以在Redis的配置文件中添加如下配置信息

appendonly yes

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

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

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

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

优点:

  • 相对RDB来说,信息丢失的较少

缺点:

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

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

Redis的AOF为减少日志文件的大小,支持AOF rewrite
简单来说,就是将日志中无效的语句删除,能够减少占用的空间

Redis存储原理

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

"散列表"

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

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

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

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

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

得到的结果是0~16383

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

Redis集群

Redis最小状态是一台服务器

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

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

系统会面临崩溃

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

主从复制

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

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

读写分离

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

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

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

哨兵模式:故障自动切换

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

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

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

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

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

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

哨兵集群

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

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

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

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

分片集群

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

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

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

Redis0~16383号槽,

例如

MasterA负责0~5000

MasterB负责5001~10000

MasterC负责10001~16383

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

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

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

Redis分布式锁的解决方案(redission)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值