文章目录
前言
提示:这里可以添加本文要记录的大概内容:
本人目前写的所有文章都是基于
springboot
项目中可能用到的框架技术,如有需要,可在专栏中寻找。
提示:以下是本篇文章正文内容,下面案例可供参考
一、关于Redis
Redis是一款基于内存使用了类似K-V结构来实现缓存数据的NoSQL非关系型数据库。
提示:Redis本身也会做数据持久化处理。
二、Redis的简单操作
Redis安装链接: https://pan.baidu.com/s/1V9MBzjfXmDyTRdAc8rf7Nw 提取码: 8y3l 当已经安装Redis,并确保环境变量可用后,可以在命令提示符窗口(CMD)或终端(IDEA的Terminal,或MacOS/Linux的命令窗口)中执行相关命令。
另外,推荐安装Redis Desktop Manager软件,可随时查看redis缓存中数据,安装链接: https://pan.baidu.com/s/1b8Nof3K6LGIvwBfqmS_8LQ 提取码: t1y0
在终端下,可以通过
redis-cli
登录Redis客户端:
redis-cli
在Redis客户端中,可以通过
ping
检测Redis是否正常工作,将得到
PONG
的反馈:
ping
在Redis客户端中,可以通过
set
命令向Redis中
存入
或
修改
简单类型的数据:
set name jack
在Redis客户端中,可以通过
get
命令从Redis中
取出
简单类型的数据:
get name
如果使用的Key并不存在,使用
get
命令时,得到的结果将是
(nil)
,等效于Java中的
null
在Redis客户端中,可以通过
keys
命令检索Key:
keys *
keys a*
注意:默认情况下,Redis是单线程的,
keys
命令会执行整个Redis的检索,所以,执行时间可能较长,可能导致阻塞!
三、在Spring Boot项目中读写Redis
首先,需要添加
spring-boot-starter-data-redis
依赖项:
<!-- Spring Data Redis:读写Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
以上依赖项默认会连接
localhost:6379
,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在
application.properties
/
application.yml
中添加任何配置就可以直接编程。如果需要显式的配置,各配置项的属性名分别为:
-
spring.redis.host
-
spring.redis.port
-
spring.redis.username
-
spring.redis.password
在使用以上依赖项实现Redis编程时,需要使用到的工具类型为
RedisTemplate
,调用此类的对象的方法,即可实现读写Redis中的数据。
在使用之前,应该先在配置类中使用
@Bean
方法创建
RedisTemplate
,并实现对
RedisTemplate
的基础配置,则在项目的根包下创建
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 {
@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测试
// 使用Redis时,Key是自由命名的
// 建议Key(名称)是分多段的
// 例如“品牌列表”,应该由 brand 和 list 这2个单词组成
// 并且,多个单词之间推荐使用英文的冒号进行分隔,例如:brand:list
// 对于同一种类型的数据,Key的第1段应该是相同的
// 例如,id=6对应的品牌数据的Key应该中:brand:item:6
// keys brand*
@Slf4j
@SpringBootTest
public class RedisTemplateTests {
@Autowired
RedisTemplate<String, Serializable> redisTemplate;
@Test
void testValueOpsSetObject() {
// ValueOperations:用于实现string(Redis)的读写
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
// 向Redis中“存入” / “修改”数据
String key = "brand:item:1";
Brand brand = new Brand();
brand.setId(1L);
brand.setName("大白象");
brand.setEnable(1);
ops.set(key, brand);
log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, brand);
}
@Test
void testValueOpsGetObject() {
// ValueOperations:用于实现string(Redis)的读写
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
// 从Redis中读取数据
String key = "brand:item:1";
Serializable value = ops.get(key);
log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
log.debug("读取到的数据的类型是:{}", value.getClass().getName());
Brand brand = (Brand) value;
log.debug("执行类型转换成功:{}", brand);
}
@Test
void testDelete() {
String key = "brand:item:1";
Boolean result = redisTemplate.delete(key);
log.debug("在Redis中删除了Key={}的数据,结果为:{}", key, result);
}
@Test
void testListRightPush() {
//向redis中写入列表数据
List<Brand> brands = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Brand brand = new Brand();
brand.setId(i + 0L);
brand.setName("测试品牌" + i);
brands.add(brand);
}
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
//所有对象共用一个key
String key = "brand:list";
for (Brand brand : brands) {
ops.rightPush(key, brand);
}
}
@Test
void testListRange() {
// 取出列表
// 在列表中的每个元素都有2个下标
// 正数的下标是从第1个元素以0作为下标,开始递增的编号
// 负数的下标是从最后一个元素以-1作为下标,开始递减的编号
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
String key = "brand:list";
long start = 0L;
long end = -1L;
List<Serializable> list = ops.range(key, start, end);
log.debug("从Redis中获取到的结果:{}", list);
for (Serializable serializable : list) {
log.debug("{}", serializable);
}
}
}
四、在项目中应用Redis
Redis是用于处理“缓存”的,当客户端尝试查询某些数据时,服务器端的处理流程大致是:
-
优先从Redis中获取数据
- 如果Redis中没有所需的数据,则从数据库中查询,并将查询结果存入到Redis
- 将Redis中的数据(或:刚刚从数据库中查询出来的数据)响应到客户端
使用Redis后,可以明显的提高查询效率(当数据表中的数据量大时,效果明显),同时,还能减轻数据库服务器的压力。
在使用之前,还应该确定需要将哪些数据使用Redis处理查询,通常,应该是查询频率可能较高的、允许数据不够准确的(即使数据有一些不准确,但是对整个项目没有严重后果的),甚至这些数据极少改变的。
在具体使用时,可以直接使用
RedisTemplate
去操作Redis,也可以对
RedisTemplate
的使用进行再次封装。
在项目中,常见做法是在根包下创建repository包,创建处理数据缓存的仓库接口,这里以品牌数据为例:
/**
* 处理品牌数据缓存的仓库接口
*/
public interface IBrandRedisRepository {
/**
* 品牌数据项的KEY的前缀
*/
String BRAND_ITEM_PREFIX = "brand:item:";
/**
* 品牌列表的KEY
*/
String BRAND_LIST_KEY = "brand:list";
/**
* 向Redis中存入数据
* @param brand
*/
void put(BrandStandardVO brand);
/**
* 向Redis中存入品牌数据,或替换原有数据,此次存入的数据仅在某段时间内有效的
*
* @param brand 品牌数据
* @param t 存活时间值
* @param timeUnit 存活时间单位
*/
void put(BrandStandardVO brand, long t, TimeUnit timeUnit);
/**
* 根据id从redis中取出数据
* @param id
* @return
*/
BrandStandardVO get(Long id);
/**
* 向Redis中存入品牌列表
*
* @param brands 品牌列表
*/
void putList(List<BrandListItemVO> brands);
/**
* 从Redis中读取品牌列表
*
* @return 品牌列表,如果Redis中没有品牌列表,则返回长度为0的集合
*/
List<BrandListItemVO> getList();
/**
* 删除缓存中所有keys内容
* @param keys
* @return
*/
Long deleteAll(Collection<String> keys);
/**
* 获取所有keys
* @return
*/
Set<String> getAllKeys();
}
接着在repository包下创建子包impl,实现接口,实现从redis中读写数据方法。
@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
public BrandRedisRepositoryImpl() {
log.info("创建了处理缓存的类:BrandRedisRepositoryImpl");
}
@Override
public void put(BrandStandardVO brand) {
String key = BRAND_ITEM_PREFIX + brand.getId();
redisTemplate.opsForValue().set(key,brand);
}
@Override
public void put(BrandStandardVO brand, long t, TimeUnit timeUnit) {
String key = BRAND_ITEM_PREFIX + brand.getId();
redisTemplate.opsForValue().set(key,brand,t,timeUnit);
}
@Override
public BrandStandardVO get(Long id) {
String key = BRAND_ITEM_PREFIX + id;
Serializable serializable = redisTemplate.opsForValue().get(key);
BrandStandardVO brandStandardVO = null;
if (serializable != null){
brandStandardVO=(BrandStandardVO) serializable;
}
return brandStandardVO;
}
@Override
public void putList(List<BrandListItemVO> brands) {
for (BrandListItemVO brand : brands) {
redisTemplate.opsForList().rightPush(BRAND_LIST_KEY, brand);
}
}
@Override
public List<BrandListItemVO> getList() {
List<Serializable> list = redisTemplate.opsForList().range(BRAND_LIST_KEY,0,-1);
List<BrandListItemVO> brands = new ArrayList<>();
for (Serializable serializable : list) {
brands.add((BrandListItemVO) serializable);
}
return brands;
}
@Override
public Set<String> getAllKeys() {
//获取所有brand的keys
return redisTemplate.keys("brand:*");
}
@Override
public Long deleteAll(Collection<String> keys) {
return redisTemplate.delete(keys);
}
}
最后在service实现类中调用,如果redis中不存在,则通过mapper从数据库中查询,再存入redis中,这里以查询详情和查询列表为例。
@Autowired
private BrandMapper brandMapper;
@Autowired
private IBrandRedisRepository brandRedisRepository;
@Override
public BrandStandardVO getStandardById(Long id) {
log.debug("开始处理【查询品牌详情】的业务");
//根据id从redis中获取数据
//判断结果是否不为null
//是:直接返回
BrandStandardVO brandStandard = brandRedisRepository.get(id);
if (brandStandard != null){
log.debug("redis中有此数据,直接返回:{}",brandStandard);
return brandStandard;
}
//redis中无此数据,调用mapper查询
//判断结果是否为null
//是:抛出异常
//否:将查询结果存入redis,并返回此结果
BrandStandardVO brandStandardVO=brandMapper.getStandardById(id);
if (brandStandardVO==null){
String message = "查询品牌详情失败,尝试访问的数据不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
log.debug("向redis中存入数据:{}",brandStandardVO);
brandRedisRepository.put(brandStandardVO,1, TimeUnit.MINUTES);
return brandStandardVO;
}
@Override
public List<BrandListItemVO> list() {
log.debug("开始处理【查询品牌列表】的业务");
//从redis中读取品牌列表
List<BrandListItemVO> brands = brandRedisRepository.getList();
//如果读取到有效列表,表示redis中存在
if (brands.size() > 0){
// 直接返回
return brands;
}
// 如果读取到的结果为空列表,表示Redis中无此数据
// 调用Mapper从数据库中查询,并存入到Redis,并返回
List<BrandListItemVO> list=brandMapper.list();
brandRedisRepository.putList(list);
return list;
}
五、关于缓存预热
缓存预热:启动项目时,就将缓存数据加载到Redis中。
在Spring Boot项目中,当需要实现“启动项目时直接执行”的效果,需要自定义组件类,实现
ApplicationRunner
接口,重写其中的
run()
方法,此
run()
将在项目启动成功后
自动执行
。
提示:缓存预热的操作应该通过
ApplicationRunner
来实现,这样才可以保证在所有组件都已经正确的创建后再执行缓存预热,如果通过某些组件的构造方法来编写缓存预热的代码,此时某些组件可能还没有创建,则无法正确执行。
关于缓存预热的具体实现:
-
删除所有相关的缓存数据
- 删除列表数据:如果不删除,再次向缓存中写入列表,将是在原列表的基础上追加,则会产生重复的列表项
- 删除数据项(每一个数据):如果不删除,则会导致原本已经缓存的数据一直存在,某些数据可能在数据库中已经删除,则缓存中的数据也应该被删除
- 从数据库中查询列表,并写入到缓存
- 基于查询到的列表,遍历,得到每个数据的id,再从数据库中查出各数据,并写入到缓存
在项目根包下,创建preload包,创建CacheLoader类实现
ApplicationRunner
接口:
@Component
@Slf4j
public class CacheLoader implements ApplicationRunner {
@Autowired
private IBrandService brandService;
public CacheLoader() {
log.debug("创建缓存加载器ApplicationRunner:CacheLoader");
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("CacheLoader.run()");
log.debug("预加载品牌数据到缓存");
brandService.loadBrandToCache();
}
}
在service实现类中:
@Override
public void loadBrandToCache() {
// 删除原有的缓存的品牌数据
Set<String> keys = brandRedisRepository.getAllKeys();
brandRedisRepository.deleteAll(keys);
// 调用Mapper查询品牌列表
List<BrandListItemVO> list = brandMapper.list();
// 调用RedisRepository将品牌列表写入到缓存中
brandRedisRepository.putList(list);
// 以上Mapper查询结果包含所有品牌的数据,每个数据中都有id
// 遍历Mapper查询结果,并调用Mapper根据id查询每个品牌数据
// 调用RedisRepository将查询到的数据写入到缓存中
for (BrandListItemVO brand : list) {
BrandStandardVO brandStandardVO = brandMapper.getStandardById(brand.getId());
brandRedisRepository.put(brandStandardVO);
}
}
六、关于自动更新缓存
更新缓存的策略有多种,通常使用的可以是:
-
手动更新
- 适用于数据变化频率非常低的应用场景,这些数据的缓存可以是长期存在,偶尔需要更新时,手动更新即可
-
自动更新
- 适用于数据频繁的变化,通过手动更新不太现实,将会是每间隔一段时间,或在特定的某个时间(例如每周一凌晨3点)自动更新
关于自动更新,需要使用到“计划任务”。
使用计划任务,需要自定义组件类,然后,在类中自定义方法(应该是
public
权限,返回值类型声明为
void
,参数列表为空),这个方法将作为计划任务执行的方法,在此方法上需要添加
@Scheduled
注解,并配置其执行频率或特定的执行时间,最后,还需要在配置类上使用
@EnableScheduling
注解,以开启当前项目的计划任务。
在项目根包下创建schedule包,并创建CacheSchedule类:
@Slf4j
@Component
public class CacheSchedule {
@Autowired
private IBrandService brandService;
public CacheSchedule() {
log.debug("创建计划任务对象:CacheSchedule");
}
// 关于@Scheduled常用属性
// >> fixedRate:每间隔多少毫秒执行一次
// >> fixedDelay:每延迟多少毫秒执行一次
// >> cron:使用1个字符串,其中写6-7个值,各值之间使用空格进行分隔
// >> >> 这6-7个值分别表示:秒 分 时 日 月 周 [年]
// >> >> 例如:cron = "56 34 12 20 1 ? 2230",表示“2230年1月20日12:34:56执行,无论当天是星期几”
// >> >> 以上各个位置,均可以使用星号,表示任意值,在“日”和“周”上,还可以使用问号,表示不关心具体值
// >> >> 以上各个位置,还可以使用"x/x"格式的值,例如,在"分钟"位置使用 1/5,表示分钟值为1时执行,且每5分钟执行1次
@Scheduled(fixedRate = 3 * 60 * 1000)
public void updateCache() {
log.debug("执行了CacheSchedule的计划任务……");
log.debug("加载品牌数据到缓存……");
brandService.loadBrandToCache();
}
}
然后创建配置类,配置类上使用
@EnableScheduling
注解,以开启当前项目计划任务。以上计划内容指每隔3分钟更新缓存一次。
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,从简单介绍redis的基本操刀到在项目中redis的应用,再到缓存预热的介绍,最后介绍了创建更新缓存计划内容,由浅入深逐步剖析。欢迎点赞评论交流。