关于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控制台,例如:
当提示符变成 127.0.0.1:6379>
后,表示已经登录了Redis控制台,则可以使用Redis的相关命令:
-
set KEY VALUE
:存入数据,例如:set username1 zhangsan
,如果反复使用同一个KEY执行此命令,后续存入的值会覆盖前序存入的值,相当于“修改”,所以,此命令既是新增数据的命令,也是修改数据的命令
-
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-
注意:在生产环境中,禁止使用此命令!
-
-
del KEY [KEY ...]
:删除或批量删除数据,例如:del username1
,或del username2 username3 username4
,将返回成功删除的数据量 -
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开始递减编号的,例如:
Redis编程
在Spring Boot项目中,实现Redis编程需要添加spring-boot-starter-data-redis
依赖项:
<!-- Spring Boot支持Redis编程的依赖项 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring-boot.version}</version>
</dependency>
在读写Redis中的数据时,需要使用RedisTemplate
工具类的对象,通常,会使用配置类的@Bean
方法来配置RedisTemplate
,则后续可以任何组件类通过自动装配得到RedisTemplate
,然后再调用相关API实现Redis中数据的读写!
在项目的根包下创建config.RedisConfiguration
类,并配置RedisTemplate
:
@Configuration
public class 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;
}
}
其中:
redisTemplate.setKeySerializer(RedisSerializer.string());//序列化为字符串
key的序列话器。在程序访问redis的数据的时候,程序在一台服务器上,redis在另外一台服务器上,中间通过网络传输的时候是通过二进制传输,所以需要设置序列化器来还原数据的类型。
redisTemplate.setValueSerializer(RedisSerializer.json()); //序列化为json
因为值的类型有很多,有可能是类,有可能是一个品牌,所以需要用json。实现一个对象通过json往redis里面存,redis在通过json取出来对应的对象。
redisTemplate.setConnectionFactory(redisConnectionFactory);//连接工厂
相当于我们再用数据库编程配置datasource:
在我们添加依赖的时候,这个依赖项自动帮我们配了一个这个连接工厂,RedisConnectionFactory redisConnectionFactory。所以我们所spring容器里面有,而当前的方法又是自动被spring容器调用的方法,就可以在参数里直接声明,这也是一种自动装配。回到之前说的什么是自动装配呢,当你的属性需要值,spring方法自动调用的参数需要值,就可以直接写进来,自动给值。
通过测试对redis做写读删等操作:
@SpringBootTest
public class RedisTests {
// 如果操作与值相关,需要获取XxxOperations才能调用对应的API
// -- 例如存入或取出字符串、对象类型的值时,需要先获取ValueOperations对象
// 如果操作与值无关,直接调用RedisTemplate的API即可
// -- 例如执行keys或delete时,直接调用RedisTemplate的API,并不需要事先获取XxxOperations对象
@Autowired
RedisTemplate<String, Serializable> redisTemplate;
// 存入字符串类型的值
@Test
void setValue() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
ops.set("username1", "张三");
System.out.println("向Redis中存入数据,完成!");
}
// 取出字符串类型的值
@Test
void getValue() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "username1";
Serializable username1 = ops.get(key);
System.out.println("根据Key=" + key + "取出数据:" + username1);
}
// 存入对象值
@Test
void setObjectValue() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
Brand brand = new Brand();
brand.setId(666L);
brand.setName("华为");
ops.set("brand666", brand);
System.out.println("向Redis中存入数据,完成!");
}
// 取出对象值
@Test
void getObjectValue() {
ValueOperations<String, Serializable> 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<String> 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<String> 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<Brand> 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<String, Serializable> 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<String, Serializable> ops = redisTemplate.opsForList();
List<Serializable> list = ops.range(key, start, end);
System.out.println("从Redis中读取list完成,数据量:" + list.size());
for (Serializable serializable : list) {
System.out.println(serializable);
}
}
}
控制台看序列化进去的json的数据比较乱:
可以用这个工具可视化查看redis里面的数据:
json数据也可以很清楚的看见:
案例,将查询从数据库查改为从redis里面查(以下只为简单案例,时间开发为建立dao层来专门做redis的数据访问):
关于Key的格式
通常,在使用Redis时,建议使用多段名称组成的Key,并且,建议使用冒号(:
)作为分隔符号!
在绝大部分Redis可视化管理工具(例如Another Redis Desktop Manager)中,默认根据冒号作为分隔符,将相同前缀的Key显示在同一个“文件夹”中。
注意:即使不使用冒号,改为使用其它符号,也是可以的,并且,大多软件可以设置分隔符号,例如:
无论使用什么符号作为Key的多段名称中间的分隔符,对于读写Redis数据,并没有任何影响!
关于Key的定义,应该是多层级的,并且,应该保证同类的数据一定具有相同的组成部分,不同类的数据一定与其它数据能明确的区分开来!
例如:
-
品牌数据详情:
brand:item:1
、brand:item:2
-
品牌列表:
brand:list
-
类别数据详情:
category:item:1
、category: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: 执行间隔,从[上一次执行结束时的时间]来计算下次的执行时间,以毫秒为单位
@Scheduled(fixedRate = 5 * 1000)
public void xxx() {
log.debug("CacheSchedule.xxx()");
}
}