前言
看了几天Redis的理论知识,发现还是不知为何物。对于没有什么概念的事物,最好的方式就是直接用一用。所以,我就决定创建一个demo来实际使用一下Redis,这样先建立一个对于Redis的直观感受。
这样就有了一个比较明确的目标:使用Redis。

演示项目

项目结构

redis缓存9万数据用了30分钟 redis缓存多久_mysql

简要介绍:
编写一个单表查询接口,根据id来查询数据。在此基础上引入Redis,作为缓存使用,体验Redis作为缓存来使用的好处。

MySQL模式

这里忽略了响应的部分,只看请求部分即可。客户端发起一个请求,经过服务器,然后从数据库查询数据,最后返回结果。

redis缓存9万数据用了30分钟 redis缓存多久_mysql_02

MySQL+Redis模式
这图是从v3.0-JavaGuide面试突击版.pdf上面截取的,我觉得它画的非常好,就直接引用了 。
这里项目的架构就变了,中间多了一层Redis,架构趋于复杂了。 现在的客户端发起一个请求,并不是直接去MySQL数据库中查询了,而是先去Redis中查询,如果查询到了就直接返回。如果没有查询到了,就再去MySQL数据库查询,并把结果加入Redis(缓存到Redis中)。

redis缓存9万数据用了30分钟 redis缓存多久_Redis_03

说明
这里添加了Redis之后,访问就变得复杂了。你可能会问,为什么要多加一个Redis呢?这里只提一点:Redis是基于内存的,数据存储在内存中;传统的数据库是基于外存(硬盘),数据存储在硬盘上。学习过操作系统的都会知道,内存和外存访问速度差距。

据说:Redis单机读写性能可以接近10万每秒,但是我现在也不会测试,反正要明白一点,它就是快!

redis缓存9万数据用了30分钟 redis缓存多久_Redis_04

安装MySQL和Redis

具体安装方式,这里就不介绍了。

建库、建表

/*
SQLyog Community v13.1.6 (64 bit)
MySQL - 5.7.29-0ubuntu0.18.04.1 : Database - poem_land
*********************************************************************
*/

/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`poem_land` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `poem_land`;

/*Table structure for table `poem` */

DROP TABLE IF EXISTS `poem`;

CREATE TABLE `poem` (
  `id` bigint(10) NOT NULL AUTO_INCREMENT,
  `title` varchar(30) DEFAULT NULL,
  `era` varchar(20) DEFAULT NULL,
  `author` varchar(20) DEFAULT NULL,
  `content` text,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

导入依赖

创建一个SpringBoot项目,导入如下依赖:

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<!-- webmagic框架 -->
	  	<dependency>
		    <groupId>us.codecraft</groupId>
		    <artifactId>webmagic-core</artifactId>
		    <version>0.7.3</version>
		    <!-- 排除掉webmagic自带的日志包,防止jar包冲突 -->
		    <exclusions>
		    	<exclusion>
			    	<groupId>org.slf4j</groupId>
	            	<artifactId>slf4j-log4j12</artifactId>
		    	</exclusion>
		    </exclusions>
		</dependency>
		
		<dependency>
		    <groupId>us.codecraft</groupId>
		    <artifactId>webmagic-extension</artifactId>
		    <version>0.7.3</version>
		</dependency>
		
		<!-- redis -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		
		<!-- mybatis plus's denpendency -->
		<dependency>
	        <groupId>com.baomidou</groupId>
	        <artifactId>mybatis-plus-boot-starter</artifactId>
	        <version>3.4.0</version>
    	</dependency>
	</dependencies>

爬取数据

俗话说,巧妇难为无米之炊。既然要查询数据,总要有数据才行吧!所以就在SpringBoot基础之上集成了WebMagic框架,但是我这里有一个特殊的用法。我并不是像别人一样是将爬虫模块作为一个定时任务一样,定时来爬取内容。我是在项目启动之后,然后执行一次爬取,爬取100首词,之后爬虫就不再运行。本来如果只是执行一次的话,应该将爬虫从SpringBoot中剥离出来,单独执行。但是我也是第一次和SpringBoot做集成,所以还是想要尝试一下。

这里的缺点是:

  1. 必须需要等待数据爬取完毕才可以执行查询(因为只是100首词,所以时间很快)。
  2. 如果需要重复启动项目或者执行了热更新操作,数据会重复,所以推荐可以使用:truncate table poem 清空表中的数据。

顺便说一下,选择爬取古诗/词是因为它们很有趣,生活不止要有技术,还要有诗、远方和佳人(现在还没有)!

知识分子的真正陷阱是沦入过度专业化与技术化的陷阱,失去了对更广阔世界的好奇心。——哈耶克

PoemPageProcessor类
这个类用于实现古诗/词的爬取逻辑,由于目标网站的限制。在不登陆的状态下,只能获取10页的内容,不过10页有100首词我觉得也就够了。

package com.dragon.task;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.stereotype.Component;

import com.dragon.entity.Poem;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

@Component
public class PoemPageProcessor implements PageProcessor {

	private Site site = Site.me().addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
			+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36");
	
	@Override
	public void process(Page page) {
		
		String currentUrl = page.getUrl().toString();
		
		// webmagic的css选择器对于文本处理有点鸡肋了,还是使用JSoup方便一点。
		String html = page.getHtml().toString();
		Document doc = Jsoup.parse(html);
		
		List<String> titleList = doc.select("body > div.main3 > div.left div.cont > p:nth-child(2)")
								    .stream()
								    .map(Element::text)         
								    .collect(Collectors.toList());
		
		
		List<String> eraList = doc.select("body > div.main3 > div.left  div.cont > p.source > a:nth-child(1)")
								  .stream()
								  .map(Element::text)
								  .collect(Collectors.toList());
		
		List<String> authorList = doc.select("body > div.main3 > div.left  div.cont > p.source > a:nth-child(3)")
									 .stream()
									 .map(Element::text)
									 .collect(Collectors.toList());
		
		List<String> contentList = doc.select("body > div.main3 > div.left  div.cont > div.contson")
									  .stream()
									  .map(Element::text)
									  .collect(Collectors.toList());
		
		String nextUrl = page.getHtml().$("#FromPage > div > a.amore").links().get();
		
		// 还是要加一个日志,才行,不然报错了看不到,它居然没处理!
		int len = titleList.size();
		List<Poem> poemList = new ArrayList<>();
		for (int i = 0; i < len; i++) {
			Poem poem = new Poem();
			poem.setTitle(titleList.get(i));
			poem.setEra(eraList.get(i));
			poem.setAuthor(authorList.get(i));
			poem.setContent(contentList.get(i));
			poemList.add(poem);
		}
		
		page.putField("poemList", poemList);
		
		// 如果当前页是第10页,说明爬取结束,不再添加url
		if (currentUrl.contains("default_4A222222222222A10")) {
			return ; // 爬取到第十页直接返回
		} else {
			page.addTargetRequest(nextUrl); // 服务器限制,不登陆只能爬取10页
		}
		
	}

	@Override
	public Site getSite() {
		return site;
	}
}

PoemSpider类
这个类实现了CommandLineRunner接口,所以它会在项目启动后执行run方法。我即在此处启动我的爬虫代码,爬取数据写入数据库中。

package com.dragon.task;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import com.dragon.service.PoemService;

import us.codecraft.webmagic.Spider;

@Component
public class PoemSpider implements CommandLineRunner {
	
	@Autowired
	private PoemPageProcessor poemPageProcessor;
	
	@Autowired
	private PoemService poemService;
	
	@Override
	public void run(String... args) throws Exception {
		Spider.create(poemPageProcessor)
			  .addUrl("https://so.gushiwen.cn/shiwen/default_4A222222222222A1.aspx")
			  .addPipeline(poemService)
			  .thread(2)
			  .start();
	}

}

PoemService接口
这个类主要是使用了Mybatis-plus提供的接口,实现对于单表的操作。以及最重要的Pipeline接口,它是实现WebMagic向数据库写入的关键,必须实现这个接口才可以执行收集数据的持久化操作。

package com.dragon.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.dragon.entity.Poem;

import us.codecraft.webmagic.pipeline.Pipeline;

public interface PoemService extends IService<Poem>, Pipeline {

}

PoemServiceImpl类
这个PoemServiceImpl在这里重写了Pipleline的process()方法,它可以实现向数据库写入数据,即数据持久化功能。

package com.dragon.service.impl;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dragon.entity.Poem;
import com.dragon.mapper.PoemMapper;
import com.dragon.service.PoemService;

import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;

@Service
public class PoemServiceImpl extends ServiceImpl<PoemMapper, Poem> implements PoemService {
	
	@Override
	@Transactional
	public void process(ResultItems resultItems, Task task) {
		Map<String, Object> map = resultItems.getAll();
		// 抑制unchecked警告
		@SuppressWarnings("unchecked")
		List<Poem> poemList = (List<Poem>) map.get("poemList");	
		this.saveBatch(poemList);
	}
}

Poem实体类

package com.dragon.entity;

import org.springframework.stereotype.Component;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

@Component
public class Poem {
	
	@TableId(type = IdType.AUTO)
	private Long id;
	private String title;
	private String era;
	private String author;
	private String content;
	// 此处省略getter、setter、toString
}

PoemMapper接口

package com.dragon.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dragon.entity.Poem;

public interface PoemMapper extends BaseMapper<Poem> {

}

PoemController类
最重要的类,也是项目中使用到了Redis的地方。这里直接看注释吧,写得已经很详细了。

package com.dragon.controller;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSONObject;
import com.dragon.entity.Poem;
import com.dragon.service.PoemService;

@RestController
@RequestMapping("/poem")
public class PoemController {
	
	@Autowired
	private PoemService poemService;
	
	@Autowired
	private RedisTemplate<String, String> redisTemplate;
	
	@GetMapping("/{id}")
	public Poem queryPoemById(@PathVariable String id) {
		// 先尝试从redis缓存中读取
		String value = redisTemplate.opsForValue().get(id);
		Poem poem = null;
		// 缓存中读取不到,再从数据库中读取
		if (Objects.isNull(value)) {
			
			System.out.println("redis缓存未命中,从mysql中读取。。。");
			
			poem = poemService.getById(id);
			if (Objects.isNull(poem)) {
				poem = new Poem();
				poem.setTitle("404 Not Found!");
				poem.setEra("unknown");
				poem.setAuthor("佚名");
				poem.setContent("路漫漫其修远兮,吾将上下而求索。");
			}
			
			// 添加无法访问到的key,注意这里容易导致缓存穿透,所以设置了过期时间
			value = JSONObject.toJSONString(poem);
			redisTemplate.opsForValue().set(id, value, 60, TimeUnit.SECONDS);
			System.out.println("将数据添加到redis中");  // 包括根本不存在的数据,不过这样会导致刚更新的数据无法访问,
			                                        // 所以过期时间不能太长
		} else {
			poem = JSONObject.parseObject(value, Poem.class);
			System.out.println("直接从redis中读取数据。。。");
		}
		return poem;
	}
}

PoemLandApplication启动类

package com.dragon;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.dragon.mapper")
public class PoemLandApplication {

	public static void main(String[] args) {
		SpringApplication.run(PoemLandApplication.class, args);
	}

}

启动测试

启动日志

redis缓存9万数据用了30分钟 redis缓存多久_spring_05


注意:爬虫模块是在项目启动后执行的。数据库中数据也已经到手了

redis缓存9万数据用了30分钟 redis缓存多久_redis缓存9万数据用了30分钟_06

启动浏览器准备测试,推荐使用FireFox,它自带json格式化!

第一次读取,缓存中没有,只能从数据库中查询。

redis缓存9万数据用了30分钟 redis缓存多久_mysql_07

redis缓存9万数据用了30分钟 redis缓存多久_redis_08

Redis中已经缓存了数据:

redis缓存9万数据用了30分钟 redis缓存多久_spring_09

再次访问:

这里可以多测试几次,我经过测试发现,缓存之后确实速度快了不少。但是你不能只看我这里给出的两次结果作为比较,那样不严谨。这里最多只是证明了Redis缓存数据之后,访问比较快。至于快多少,这里的测试太简单了,显然是不能给出一个准确的答案的。

redis缓存9万数据用了30分钟 redis缓存多久_redis_10


redis缓存9万数据用了30分钟 redis缓存多久_spring_11

说明

这里只是对于Redis的一个简单使用。Redis是一个key-value的nosql数据库。它总共有五种数据类型:string、hash、list、set、zset。这里我使用的即是string,不过这也很正常,string也确实是最常用的(我听B站视频里面up主说的)。这里我主要的目的是为了学习,建立一个对于Redis的直观概念,知道它能干啥,能够简单使用它。现在它的目的差不过也达到了,至于更加复杂的使用,以后学习的时候,需要逐渐积累来学习。

代码上传到了GitHub上面,如果感兴趣的话,就去clone吧!以后可能还会更新,使用这个项目来实践其它的技术。


PS

这里你可能会想到,为什么不用java自身提供的类库Map来缓存数据呢?那这样的话,上面的五种数据类型,也就可以使用以下类型代替了:

string --> Map<String, String>
hash --> Map<String, Map<String, String>>
list --> Map<String, List<String>>
set --> Map<String, Set<String>>
zset --> Map<String, TreeSet<String>>

这也做,确实是可以的,而且它的速度会更快。因为Redis服务一般和服务器都是分开部署的,通过网络协议通信,而这个是内存直接访问的。

但是它也具有如下缺点:
引用自v3.0-JavaGuide面试突击版.pdf

缓存分为本地缓存和分布式缓存。以 Java 为例,使⽤⾃带的 map 或者 guava 实现的是本地缓存,最
主要的特点是轻量以及快速,⽣命周期随着 jvm 的销毁⽽结束,并且在多实例的情况下,每个实例都 需要各⾃保存⼀份缓存,缓存不具有⼀致性。
使⽤ redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共⽤⼀份缓存数据,缓 存具有⼀致性。缺点是需要保持redis 或 memcached服务的⾼可⽤,整个程序架构上较为复杂。