Java阶段四Day10
文章目录
- Java阶段四Day10
- 关于Redis
- Redis的数据类型
- Redis中的list类型
- Redis的常用命令
- 关于Key的格式
- Redis编程
- 使用Redis时的数据一致性问题
- 关于ApplicationRunner
- LoadCacheRunner
- ContentCategoryServiceImpl
- 计划任务
- ScheduleConfiguration
- CategoryCacheSchedule
关于Redis
Redis是一种基于内存的,使用K-V结构存储数据的NoSQL非关系型数据库。
提示:Redis也会占用磁盘空间,并且,会自动将内存中的数据同步到硬盘上,所以,存入到Redis中的数据,即使重启电脑,再次开机时Redis会自动将硬盘上的数据重新加载到内存,但是,正常读写Redis仍是在内存中处理的!
Redis的主要作用是缓存数据,通常,会将关系型数据库(例如MySQL)中的数据读取出来,并写入到Redis中,后续,当需要获取数据时,直接从Redis中读取即可,不必再从关系型数据库中读取数据,从而提高获取数据的效率!
由于Redis是基于内存的,读写效率远高于基于磁盘(例如硬盘)的关系型数据库,使得单次查询耗时变得非常短,则整个系统处理数据的效率会明显提升,并且,由于减少了对关系型数据库的访问频率,可以起到“保护”关系型数据库的作用!
Redis的数据类型
Redis中的经典数据类型有5种:string / hash / list / set / z-set
- 在Java语言中的简单数据类型,在Redis中对应的都是string
另外,还有:bitmap / hyperloglog / GEO / 流
Redis中的list类型
在Redis中,list类型的数据是一个先进后出、后进先出的栈结构:
在学习Redis时,你需要将以上图例旋转了90度!
可以选择从左侧压栈来存入数据,例如:
可以选择从右侧压栈来存入数据,例如:
从Redis中读取list数据时,始终是从左至右读取的,通常,为了更加符合使用列表的习惯,大多情况下采取“从XX压入数据”的做法。
**注意:**在Redis中的list数据,每个元素都同时拥有2个下标,一个是从左至右、从0开始递增编号的,另一个是从右至左、从-1开始递减编号的!
**注意:**在读取List中的区间时,end
表示的元素不可以是相对start
更靠左的元素!
提示:-1
始终是最后一个元素的下标,所以,当你需要读取整个列表的数据时,start
为0
,end
为-1
。
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
:清空当前数据库
更多命令可参考:Redis命令手册
关于Key的格式
Redis本身并没有要求Key应该如何定义,但是,由于Redis被设计为可以存放特别多数据,并且,各种各样的数据都可以放在Redis中,所以,在实际应用中,应该使用有规律的Key,否则,后续读写数据时会很麻烦!
例如:当存放“用户”相关数据时,应该使用user
字样作为Key的一部分,存放“文章”相关数据时,应该使用article
字样作为Key的一部分。
示例:假设有20个用户数据,ID分别是从1到20,使用Redis中的string类型分别存储这20个用户数据时,各数据对应的Key可以是:user-1
、user-2
、……、user-20
,后续,只要知道被获取数据的ID,就可以知道Key,并基于这个Key来读取数据。
在定义Key时,各部分之间应该使用某个符号进行分隔,例如user-1
就是在user
前缀和ID之间使用了减号进行分隔!
在绝大部分Redis可视化工具(例如:Another Redis Desktop Manager)中,默认情况下,会自动处理Key中的冒号(:
),并多个前缀相同的Key放在相同的“文件夹”中,例如:
以上冒号只是建议使用的分隔符号,并不是必须使用冒号,也可以改为其它符号,例如减号等,大多软件也可以设置根据什么符号来分隔出“文件夹”。
提示:无论使用什么分隔符,只要Key不冲突,对使用是没有影响的,所以,并不一定需要分隔符,只是强烈建议在Key中使用分隔符。
在开发实践中,Key的定义应该是多层级的,并且,应该保证同类数据一定具有相同的组成部分,例如:
- 用户详情数据:
user:item:1
、user:item:2
- 用户列表数据:
user:list
- 文章详情数据:
article:item:1
、article:item:2
- 文章列表数据:
article:list
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的配置类
*
*/
@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时的数据一致性问题
在开发实践中,数据最终肯定是保存在关系型数据库(例如MySQL等)中的,同时,为了提高查询效率、保护关系型数据库,通常会将某些数据从关系型数据库中读出来,并写入到Redis中,后续,将优先从Redis中读取数据!
由于在关系型数据库和Redis中都存储了数据,如果某个数据需要修改,最终修改的肯定是关系型数据库中的数据,但是,如果Redis中的数据没有及时更新,却仍从Redis中读取数据,则读取到的数据就是“不准确的”!
所以,当同一个数据在多个不同的位置存储了多份,就可能出现以上问题,通常称之为“数据一致性”的问题,即2个或多个不同的存储位置,本应该“相同”的数据其实“并不相同”!
关于数据一致性问题:
- 并不一定有必要及时更新数据,例如:MySQL中的数据发生了变化,但是Redis中却不更新,此时,数据并不一致,但是,对于软件的使用可能没有严重影响
- 例如:某个热门视频的播放次数
- 例如:购买火车票时,列表页面中显示的的各车次的余票数量
- 某些数据的更新频率可能非常低,这类数据基本上没有数据一致性问题
- 例如:全国省市区数据
- 并不是所有数据都适合在Redis中也存一份,对于访问频率非常低的数据,或数据量特别大的数据,可以不使用Redis
- 例如:用户3年前的历史订单
解决数据一致性问题的做法主要有:
- 即时更新:当关系型数据库的数据发生变化,马上更新Redis中的数据
- 保证数据的实时一致性
- 周期性更新:当关系型数据库的数据发生变化,不会马上更新Redis中的数据,而是每间隔一段时间,或到了某个指定的时间时,执行1次同步数据的操作
- 保证数据的最终一致性
- 手动更新:当关系型数据库的数据发生变化,不会马上更新Redis中的数据,而是由管理人员(或运营人员)明确执行更新操作,才会同步数据
- 保证数据的最终一致性
关于ApplicationRunner
在Spring Boot中,可以自定义组件类,实现ApplicationRunner
接口,重写其中的run()
抽象方法,后续,当启动项目后,会在启动成功后的第一时间自动调用此run()
方法!
LoadCacheRunner
@Slf4j
@Component
public class LoadCacheRunner implements ApplicationRunner {
@Autowired
private ContentCategoryService categoryService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("开始执行【加载缓存数据】");
categoryService.rebuildListCache();
}
}
ContentCategoryServiceImpl
@Service
@Slf4j
public class ContentCategoryServiceImpl implements ContentCategoryService {
@Autowired
private CategoryCacheRepository categoryCacheRepository;
@Autowired
private ContentCategoryRepository categoryRepository;
public ContentCategoryServiceImpl() {
log.info("创建业务对象:CategoryServiceImpl");
}
@Override
public List<ContentCategoryListItemVO> list() {
log.debug("开始处理【查询类别数据列表】的业务,参数:无");
return categoryCacheRepository.list();
}
@Override
public void rebuildListCache() {
log.debug("开始处理【重建缓存中的类别列表】的业务,参数:无");
categoryCacheRepository.deleteList();
List<ContentCategoryListItemVO> list = categoryRepository.list();
categoryCacheRepository.save(list);
}
}
利用以上机制,可以实现“缓存预热”(项目启动时就将数据加载到缓存中)。
但其实用价值不太高,需要结合手动更新一起使用,因为只会在项目启动的那一刻执行,当数据发生改变仍需要手动更新,适合类别地址、省市区管理等,适用于数据变化频率不规则,而如何使用周期性更新就需要用到计划任务
计划任务
在Spring Boot中,默认禁止了所有的计划任务,需要在配置类上添加@EnableScheduling
注解,以启用当前项目中所有计划任务。
在项目中,自定义组件类,并在组件类中自定义方法,使用框架调用,无返回值无参数列表。在方法上添加@Scheduled
注解,则此方法就是一个计划任务方法,可以周期性的执行。
ScheduleConfiguration
@Slf4j
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
public ScheduleConfiguration() {
log.debug("创建配置类对象:ScheduleConfiguration");
}
}
CategoryCacheSchedule
/**
* 处理类别缓存的计划任务
*/
@Slf4j
@Component
public class CategoryCacheSchedule {
@Autowired
private ContentCategoryService categoryService;
public CategoryCacheSchedule() {
log.debug("创建计划任务组件对象:CategoryCacheSchedule");
}
// fixedRate:执行频率,根据上一次执行的开始时间来计算下一次的执行时间,取值以毫秒为单位
// fixedDelay:执行间隔,根据上一次执行的结束时间来计算下一次的执行时间,取值以毫秒为单位
// cron:使用cron表达式来配置执行时间
// -- cron表达式的本质是一个字符串,此字符串中包括6~7个域,每个域的值之间使用空格分隔
// -- cron表达式中的域从左至右依次是:秒 分 时 日 月 周 [年]
// -- 各域的值可以使用通配符:
// -- 使用星号(*)表示任意值
// -- 使用问号(?)表示不关心此值,此通配符只能用于“日”和“周”所在的域
// -- 在各个域中,可以使用减号表示连接的区间,例如在“月”所在的域使用 5-7 表示“从5至7”
// -- 在各个域中,可以使用英文的逗号分隔多个值,表示满足列举的任意值均可,例如在“月”所在的域使用 1,3,5,7,9 表示1月或3月或5月或7月或9月均匹配
// -- 在各个域中,数字值可以写成“x/y”的格式,表示此域的值为x时执行,且每间隔y再次执行,直至其它域的值不满足匹配
// -- 更多内容可参考:https://zhuanlan.zhihu.com/p/573018560
@Scheduled(fixedRate = 2 * 60 * 1000)
public void rebuildListCache() {
log.debug("开始执行【重建缓存中的类别列表】的计划任务");
categoryService.rebuildListCache();
}
}