0. 项目结构
整个项目结构图如下:
github源码下载
1. 剧情回顾
在 SpringBoot搭建一个简单的天气预报系统(一)博客中已经实现了一个非常简单的天气预报系统------用户访问这个天气预报系统的时候,那么系统会响应给用户一个天气数据。但这个数据的来源并不是我们自己产生的,我们是依赖于一个第三方的天气预报的数据API。通过调用这个API,我们将返回的Json数据进行解析,再返回给使用这个系统的用户。
2. 存在的问题
这种系统架构会存在什么问题呢?
- 我们的数据来源是依赖第三方的API,这种强依赖会导致系统服务的延迟(用户调用我们的系统接口时,而系统再通过HTTP(很耗时)去调用第三方的API,调用结束后,第三方接口将Json数据响应给系统,然后,系统再进行解析,并返回给系统用户,这样的结果会导致响应时间不及时)
- 我们使用的是别人的接口(虽然是免费,有些还限制调用次数),这种情况是有风险的:1)、有限制调用次数,会达到上限;2)、不稳定,随时挂掉;3)、若此第三方的API并发做的不太理想,我们这边系统并发稍微高一点,就有可能将第三方API系统给调死了。
3. SpringBoot集成Redis
3.1 解决方案
面对以上问题,这里采用Redis缓存来解决-----期望使用Redis提升应用的并发访问能力
3.2 为什么选择Redis?
这里针对此系统简要说说Redis的优点吧:
- 可以使请求响应得更及时:它是基于内存的缓存系统,查询、响应等速度很快(先将天气数据缓存到Redis中去,获取数据时,先从Redis中获取,如果有,直接返回;如果没有,再调用第三方API,调用完毕后,再把数据添加到Redis中去)
- 可以有效地减少服务调用的次数:天气的数据信息并不会实时更新,即使是真实的天气预报系统也是半小时或1小时更新,这种场景就非常适合使用缓存(半个小时就调用一次(一天12h也就调用了24次)第三方的API,将数据缓存到Redis中去,在半个小时内,都是从Redis中获取数据的)
3.3 使用Redis
3.3.1 开发环境
- IDEA:2018.2(安装lombok插件)
- JDK:8
- MAVEN:3.6.0
- SpringBoot:2.3.0
- Redis:3.0.504
3.3.2 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3.3 功能需求
用户访问这个天气预报系统的时候,系统先从Redis缓存中进行获取数据,如果有,直接返回;如果没有,再调用第三方API,调用完毕后,将数据返回,并把数据添加到Redis中去
3.3.4 手动编码
这里主要修改WeatherDataServiceImpl类中的doGetWeather()方法
WeatherDataServiceImpl
添加了个注解@Slf4j和StringRedisTemplate 成员变量
@Service
@Slf4j
public class WeatherDataServiceImpl implements WeatherDataService {
private static final String WEATHER_URL = "http://wthrcdn.etouch.cn/weather_mini?";
@Autowired
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
...
}
doGetWeather()
private <T> T doGetWeather(String uri, Class<T> type) {
String key = uri;
String strBody = null;
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 先查询缓存,如果缓存有,则从缓存中取
if (stringRedisTemplate.hasKey(key)) {
log.info("Redis has data");
strBody = ops.get(key);
} else {
log.info("Redis don't has data");
// 如果缓存没有,则再调用服务接口
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
if (StatusCodeConstant.OK == responseEntity.getStatusCodeValue()) {
strBody = responseEntity.getBody();
}
// 将数据写入缓存
ops.set(key, strBody, RedisConstant.TIME_OUT, TimeUnit.SECONDS);
}
ObjectMapper objectMapper = new ObjectMapper();
T t = null;
try {
t = objectMapper.readValue(strBody, type);
} catch (Exception e) {
log.error("Error!", e);
}
return t;
}
- 使用Redis,需要用到一个类:StringRedisTemplate,它对Redis的API做了一层封装
- Redis是K-V的数据结构。这里的K是uri,V是根据uri返回的Json数据
- 这里也使用了日志注解@Slf,使用日志进行打印
RedisConstant
public interface RedisConstant {
// 过期时间:1800s
Long TIME_OUT = 1800L;
}
3.3.5 测试
启动项目之前,先启动Redis。测试时,可以先把过期时间设置短一点,如:10s。完成之后,再可以设置成1800s。假如设置10s的话,你第一次访问,控制台会打印“Redis don’t has data”,在10s以内再次访问时,会打印“Redis has data”,因为数据已经被存储到了Redis中去了,可通过Redis的客户端工具Redis DeskTop Manager进行查看。超过10s后,再访问,就会打印“Redis don’t has data”,因为数据在Redis中过期了。
4. SpringBoot集成Quartz
Quartz是一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。
4.1 为什么要进行天气数据的同步
在此之前,这个系统使用了Redis缓存来减少了调用第三方API的次数,但我们还是对它是强依赖,毕竟我们仅仅只是数据的搬运工,并不是产生者。况且,我们也做不了产生者,我们只能依赖于第三方的天气服务商来提供数据给我们。
之前使用Redis缓存来减少调用第三方API的次数是如何做的-----当用户访问系统时,系统才会触发去调用第三方接口(缓存中没有此数据)的动作。实际上,这个动作应该自动完成,不应该等到用户请求时,再去拉取最新的数据(如果是这样的话,这就比较迟了)。
4.2 如何实现天气数据的同步呢?
实现数据同步就需要一个定时器来帮助我们实现这种定时任务,因为我们的天气数据大概半小时或1小时变更一次,那么,我们的定时器就设置为半小时或1小时去执行这个定时动作。这里的定时器就选择Quartz,它在业界算比较流行
4.3 使用Quartz
4.3.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
4.3.2 手动编码
WeatherDataSyncJob
定义了一个Job
@Slf4j
public class WeatherDataSyncJob extends QuartzJobBean {
@Autowired
private CityDataService cityDataService;
@Autowired
private WeatherDataService weatherDataService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Weather Data Sync Job. Start!");
}
}
这里用日志打印一条语句,先不做任何业务逻辑
QuartzConfig
这个配置类是用来配置Quartz的
@Configuration
public class QuartzConfig {
// 频率(多长时间执行一次)
private static final Integer TIME = 1800;
@Bean
public JobDetail weatherDataSyncJobJobDetail() {
return JobBuilder
.newJob(WeatherDataSyncJob.class)
.withIdentity("weatherDataSyncJobJobDetail")
.storeDurably()
.build();
}
@Bean
public Trigger weatherDataSyncJobTrigger() {