[java]Redis

关于Redis

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

基于内存的:Redis读写数据时,都是在内存中进行读写的,所以,读写效率非常高!另外,Redis会自动的将所管理的数据同步到硬盘上,并且,在每次启动时也会自动从硬盘上将数据同步到内存中,所以,即使计算机重启,Redis中的数据基本不上会丢失

mysql是储存在磁盘上的,硬盘的特征是永久储存,不会因为断电而丢失数据,而内存会因为断电数据全部丢失。内存条是直接和cpu交换数据的。cpu是处理数据的一个硬件,负责运算,cpu的三大核心组成部分:第一,运算器,第二,控制器,第三,高速缓存。控制器是协调计算机的作用,高速缓存是内置的一个小小的空间,用来储存中间结果的。什么是中间结果?比如2+8+5=15,我们运算的时候会先用2+8=10,再用10+5=15,这个中间的10就是中间结果。运算器就是负责算术运算和逻辑运算,逻辑运算就是是和非,或者是和否、对和错这类运算,所以cpu是负责整个计算机的运算。那cpu的数据是从哪里来的?cpu是获取的是内存中的数据。如果磁盘中的数据需要被运算,则需要先加载到内存中,在被cpu处理。所以我们的程序当中需要使用流,流是连接了内存和内存以外数据的管道,比如我们读硬盘上的数据得用流,读网络上的数据得用流。所以流一边连接了内存,一边连接了硬盘和网络。因为cpu只能解取内存上的数据,以至于内存还有一个特点,它是整个计算机硬件系统里面,读写效率最高的存储设备。

使用K-V结构:在Redis中的数据,每个数据都有一个Key,则写入数据时需要指定数据的Key,读取数据时是根据Key找到对应的数据。

NoSQL:可以理解为No SQL,或No Operation SQL,总之,就是与SQL语句无关,因为只需要根据Key就可以访问数据。

数据库:数据的仓库,在没有明确的说明之前,通常,指的就是各种关系型数据库,例如MySQL等。

非关系型数据库:存储在Redis中的所有数据之间并没有任何关系。

什么是关系型数据库呢?例如像mysql这种,一张库里面可以有多张表,表是有表的结构的,每种数据有数据属性,我们都可以通过表把它表现出来。同时表和表之间有关联,存在数据与数据之间的关联,这种就称之为关系型数据库,

Redis的主要作用是缓存数据,通常的表现为:将关系型数据库(例如MySQL)中的数据取出,存入到Redis中,后续,当程序需要读取这些数据时,不再从关系型数据库中读取,而是优先从Redis中读取。

由于Redis是基于内存的,所以,读写效率远高于基于硬盘的关系型数据库,则Redis的查询效率非常高,单次查询耗时更短,就可以承受更多的查询访问量,并减少了对关系型数据库的访问,从而起到“保护”关系型数据库的作用!

因为数据库查询量太大也是会崩的,崩了不能读写,项目也就相当于废了,所以数据库不能崩,而redis可以分摊它的压力,从而起到保护作用。

Redis中的数据类型

Redis中的经典数据类型有5种,分别是:string(一般值,例如字符串、数值等可以字面表示的) / list / set / hash(对象,对应Java中的Map) / z-set

list就是列表,有序的。set就是相当于java中的set,也有无序性的特征,无序的同时效率也会更快。hash相当于是java中的对象,或者说对应Java中的Map。z-set是有序的set。注意在java中不是所有的set都是无序的,只有hashset是无序的,LinkedHashSet都是有序的。

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

Redis中的常用命令

 在终端窗口中,可以通过redis-cli命令登录Redis控制台,例如:

[java]Redis_第1张图片

 当提示符变成 127.0.0.1:6379> 后,表示已经登录了Redis控制台,则可以使用Redis的相关命令:

  • set KEY VALUE:存入数据,例如:set username1 zhangsan,如果反复使用同一个KEY执行此命令,后续存入的值会覆盖前序存入的值,相当于“修改”,所以,此命令既是新增数据的命令,也是修改数据的命令

[java]Redis_第2张图片

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

  • keys PATTERN:根据模式(PATTERN)获取KEY,例如:keys username1,如果模式匹配的KEY是存在的,则返回匹配的KEY,如果不存在,则返回(empty list or set),在模式中,可以使用星号(*)作为通配符,例如:keys username*,将返回所有以username作为前缀的KEY的集合,甚至 ,你可以使用keys *匹配所有的KEY

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

[java]Redis_第3张图片

  • del KEY [KEY ...]:删除或批量删除数据,例如:del username1,或del username2 username3 username4,将返回成功删除的数据量

  • flushdb:清空当前数据库

[java]Redis_第4张图片

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

Redis中的List类型的数据

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

[java]Redis_第5张图片

在学习Redis时,你应该把Redis中的list结构想像成一个在以上图例的基础上旋转了90度的栈!

在操作Redis中的list时,可以从左侧进行压栈或弹栈的操作,以压栈为例:

[java]Redis_第6张图片

 还可以从右侧进行压栈或弹栈的操作,以压栈为例:

[java]Redis_第7张图片

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

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

[java]Redis_第8张图片

Redis编程

 在Spring Boot项目中,实现Redis编程需要添加spring-boot-starter-data-redis依赖项:



    org.springframework.boot
    spring-boot-starter-data-redis
    ${spring-boot.version}

在读写Redis中的数据时,需要使用RedisTemplate工具类的对象,通常,会使用配置类的@Bean方法来配置RedisTemplate,则后续可以任何组件类通过自动装配得到RedisTemplate,然后再调用相关API实现Redis中数据的读写!

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

@Configuration
public class RedisConfiguration {

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

}

其中:

redisTemplate.setKeySerializer(RedisSerializer.string());//序列化为字符串
key的序列话器。在程序访问redis的数据的时候,程序在一台服务器上,redis在另外一台服务器上,中间通过网络传输的时候是通过二进制传输,所以需要设置序列化器来还原数据的类型。

redisTemplate.setValueSerializer(RedisSerializer.json()); //序列化为json

因为值的类型有很多,有可能是类,有可能是一个品牌,所以需要用json。实现一个对象通过json往redis里面存,redis在通过json取出来对应的对象。

redisTemplate.setConnectionFactory(redisConnectionFactory);//连接工厂

相当于我们再用数据库编程配置datasource:

[java]Redis_第9张图片

在我们添加依赖的时候,这个依赖项自动帮我们配了一个这个连接工厂,RedisConnectionFactory redisConnectionFactory。所以我们所spring容器里面有,而当前的方法又是自动被spring容器调用的方法,就可以在参数里直接声明,这也是一种自动装配。回到之前说的什么是自动装配呢,当你的属性需要值,spring方法自动调用的参数需要值,就可以直接写进来,自动给值。

通过测试对redis做写读删等操作:

@SpringBootTest
public class RedisTests {

    // 如果操作与值相关,需要获取XxxOperations才能调用对应的API
    // -- 例如存入或取出字符串、对象类型的值时,需要先获取ValueOperations对象
    // 如果操作与值无关,直接调用RedisTemplate的API即可
    // -- 例如执行keys或delete时,直接调用RedisTemplate的API,并不需要事先获取XxxOperations对象
    @Autowired
    RedisTemplate redisTemplate;

    // 存入字符串类型的值
    @Test
    void setValue() {
        ValueOperations ops = redisTemplate.opsForValue();
        ops.set("username1", "张三");
        System.out.println("向Redis中存入数据,完成!");
    }

    // 取出字符串类型的值
    @Test
    void getValue() {
        ValueOperations ops = redisTemplate.opsForValue();
        String key = "username1";
        Serializable username1 = ops.get(key);
        System.out.println("根据Key=" + key + "取出数据:" + username1);
    }

    // 存入对象值
    @Test
    void setObjectValue() {
        ValueOperations ops = redisTemplate.opsForValue();
        Brand brand = new Brand();
        brand.setId(666L);
        brand.setName("华为");
        ops.set("brand666", brand);
        System.out.println("向Redis中存入数据,完成!");
    }

    // 取出对象值
    @Test
    void getObjectValue() {
        ValueOperations ops = redisTemplate.opsForValue();
        String key = "brand666";
        Serializable serializable = ops.get(key);
        Brand brand666 = (Brand) serializable;
        System.out.println("根据Key=" + key + "取出数据:" + brand666);
    }

    // 使用keys获取各个Key
    @Test
    void keys() {
        String pattern = "*";
        Set keys = redisTemplate.keys(pattern);
        System.out.println(keys);
    }

    // 删除某个数据
    @Test
    void delete() {
        String key = "username1";
        Boolean delete = redisTemplate.delete(key);
        System.out.println("根据Key=" + key + "执行删除,结果:" + delete);
    }

    // 删除多个数据
    @Test
    void deleteBatch() {
        Set keys = new HashSet<>();
        keys.add("username2");
        keys.add("username3");
        keys.add("username4");
        keys.add("username5");
        keys.add("username6");
        Long delete = redisTemplate.delete(keys);
        System.out.println("根据Keys=" + keys + "执行删除,结果:" + delete);
    }

    // 写入list数据
    @Test
    void rightPush() {
        List brandList = new ArrayList<>();
        for (int i = 1; i <= 8; i++) {
            Brand brand = new Brand();
            brand.setId(i + 0L);
            brand.setName("测试品牌" + i);
            brandList.add(brand);
        }

        ListOperations ops = redisTemplate.opsForList();
        String key = "brandList";
        for (Brand brand : brandList) {
            ops.rightPush(key, brand);
        }
        System.out.println("存入list数据,完成!");
    }

    // 读取list数据
    // 起始位置和结束位置都可以使用正数的下标或负数的下标
    // 但是,必须保证起始位置对应的元素在结束位置的元素的左侧
    @Test
    void range() {
        String key = "brandList";
        long start = 0;
        long end = -1;
        ListOperations ops = redisTemplate.opsForList();
        List list = ops.range(key, start, end);
        System.out.println("从Redis中读取list完成,数据量:" + list.size());
        for (Serializable serializable : list) {
            System.out.println(serializable);
        }
    }

}

 控制台看序列化进去的json的数据比较乱:

[java]Redis_第10张图片
可以用这个工具可视化查看redis里面的数据:
[java]Redis_第11张图片

 json数据也可以很清楚的看见:

[java]Redis_第12张图片

 

案例,将查询从数据库查改为从redis里面查(以下只为简单案例,时间开发为建立dao层来专门做redis的数据访问):

[java]Redis_第13张图片

[java]Redis_第14张图片

[java]Redis_第15张图片

关于Key的格式

通常,在使用Redis时,建议使用多段名称组成的Key,并且,建议使用冒号(:)作为分隔符号!

在绝大部分Redis可视化管理工具(例如Another Redis Desktop Manager)中,默认根据冒号作为分隔符,将相同前缀的Key显示在同一个“文件夹”中。

注意:即使不使用冒号,改为使用其它符号,也是可以的,并且,大多软件可以设置分隔符号,例如:

[java]Redis_第16张图片

 无论使用什么符号作为Key的多段名称中间的分隔符,对于读写Redis数据,并没有任何影响!

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

例如:

  • 品牌数据详情:brand:item:1brand:item:2

  • 品牌列表:brand:list

  • 类别数据详情:category:item:1category:item:2

  • 类别列表:category:list

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

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

由于在关系型数据库和缓存服务器上都存储了数据,如果某个数据发生了变化,通常是修改关系型数据库中的数据,此时,如果缓存中的数据没有及时更新,并仍从缓存中获取数据,则获取到的数据是不准确的!

如果出现了关系型数据库中的数据与缓存中的数据不同的问题,则称之为“数据一致性问题”,即2个或多个不同的存储位置中,本应该相同的数据并不相同。

其实,解决数据一致性问题的主要做法就是:更新关系型数据库中的数据时,一并更新缓存中的数据!

关于数据一致性问题:

  • 并不是所有数据都需要保证“实时一致性”,即:当关系型数据库中的数据发生变化后,并不一定需要马上更新缓存中的数据,此时,缓存中的数据是“不准确的”,但是,某些数据不并不需要完全准确

    • 例如:车票的余量(在列表中显示的值)

  • 某些数据的修改频率可能非常低,这类数据在绝大部分时间里都不会出现“数据一致性”问题

    • 例如:商品的类别

  • 某些数据的修改频率可能非常高,这类数据可能一开始就不会放在缓存中,也就没存在“数据一致性”问题

  • 某些数据的查询频率可能非常低,这类数据可能一开始就不会放在缓存中,也就没存在“数据一致性”问题

    • 例如:用户三年前的订单

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

  • 即时更新:更新关系型数据库中的数据的同时,也更新缓存中的数据

    • 可能需要考虑分布式事务

  • 周期性更新:更新关系型数据库中的数据时,不更新缓存中的数据,而是每隔一段时间更新一次缓存中的数据

  • 手动更新:更新关系型数据库中的数据时,不更新缓存中的数据,而是由管理人员明确执行“更新缓存”的操作时才更新缓存中的数据

使用ApplicationRunner实现缓存预热

当项目刚刚启动时,就直接从关系型数据库中读取数据并写入到缓存中,这种做法就称之为“缓存预热”。

在Spring Boot项目中,可以自定义组件类,实现ApplicationRunner接口,重写其中的run()方法,此方法会在项目启动后的第一时刻就自动执行!

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

@Component
public class CachePreload implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("CachePreload.run()");
    }
    
}

你可以在以上run()中编写任何你认为需要在项目启动后就自动执行的任务!同时,由于以上类是一个组件类,所以,在类中你可以按需自动装配任何在Spring容器中的对象!

计划任务

 计划任务指的是:在满足一定条件下,会周期性执行的任务,通常,可能是每过多长时间就执行一次,或到了某个特定的时间点就执行一次。

计划任务可能是比较耗时的,在Spring Boot项目中,默认不允许执行计划任务,需要在配置类上添加@EnableScheduling注解,以启用计划任务!

则在项目中的根包下创建config.ScheduleConfiguration类,在类上添加@Configuration注解,并添加@EnableScheduling注解:

@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}

然后,在任何组件类中,自定义方法(公有的、void返回值类型、无参数列表),并在方法上添加@Scheduled注解,则此方法就是一个计划任务方法,然后,配置注解参数,以决定什么时候执行计划任务,例如:

@Slf4j
@Component
public class CacheSchedule {

     // fixedRate:执行频率,以【上一次执行开始时的时间】来计算下次的执行时间,以毫秒为单位
    // fixedDelay:执行间隔,从【上一次执行结束时的时间】来计算下次的执行时间,以毫秒为单位
    // cron:取值是一个字符串,此字符串是一个表达式,包含6~7个值,各值之间使用空格分隔
    // -- 在cron表达式中,各值从左至右分别是:秒 分 时 日 月 周 [年]
    // -- 各值均可使用通配符
    // -- 使用星号(*)表示任意值
    // -- 使用问号(?)表示不关心此值,此通配符仅可以用于“日”和“周”
    // -- 各值可以使用 x/y 格式的值,其中,x表示起始值,y表示间隔周期,以“分”位置取值为1/5为例,表示“分”的值为1时开始执行,且每间隔5分钟执行一次
    // 示例:
    // "56 34 12 13 2 ? 2023"表示:2023年2月13日12:34:56执行任务,不关心当天星期几
    // "0/30 * * * * ?"表示:每分钟的0秒时执行,且每30秒执行一次
    // 更多内容参考:
    // https://segmentfault.com/a/1190000021574315
    // https://blog.csdn.net/study_665/article/details/123506946
    @Scheduled(cron = "0 0 1 5 6 MON")
    public void xxx() {
        log.debug("CacheSchedule.xxx()");
    }
}

你可能感兴趣的:(java,redis,开发语言)