springboot和redis处理页面缓存
页面缓存是应对高并发的一个比较常见的方案,当请求页面的时候,会先查询redis缓存中是否存在,若存在则直接从缓存中返回页面,否则会通过代码逻辑去渲染页面,并将渲染后的页面缓存到redis中,然后返回。下面通过简单的demo来描述这一过程:
一、准备工作:
1、新建一个springboot工程,命名为novel,添加如下依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.dtouding</groupId> <artifactId>novel</artifactId> <version>0.0.1-SNAPSHOT</version> <name>novel</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--thymeleaf--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--mysql connector--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.9</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!--jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2、创建一个novel表,并插入几条数据,如下:
DROP TABLE IF EXISTS `t_novel`; CREATE TABLE `t_novel` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '小说ID', `novel_name` varchar(16) DEFAULT NULL COMMENT '小说名称', `novel_category` varchar(64) DEFAULT NULL COMMENT '小说类别', `novel_img` varchar(64) DEFAULT NULL COMMENT '小说图片', `novel_summary` longtext COMMENT '小说简介', `novel_author` varchar(16) DEFAULT NULL COMMENT '小说作者', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; INSERT INTO `t_novel` VALUES ('5', '诛仙', '仙侠', '/img/zhuxian.jpg', '该小说以“天地不仁,以万物为刍狗”为主题,讲述了青云山下的普通少年张小凡的成长经历以及与两位奇女子凄美的爱情故事,整部小说构思巧妙、气势恢宏,开启了一个独具魅力的东方仙侠传奇架空世界,情节跌宕起伏,人物性格鲜明,将爱情、亲情、友情与波澜壮阔的正邪搏斗、命运交战汇集在一起,文笔优美,故事生动。它与小说《飘邈之旅》、《小兵传奇》并称为“网络三大奇书”,又被称为“后金庸时代的武侠圣经”。', '萧鼎'); INSERT INTO `t_novel` VALUES ('6', '英雄志', '武侠', '/img/yingxiongzhi.jpg', '《英雄志》为一虚构中国明朝历史的古典小说,借用明英宗土木堡之变为背景,以复辟为舞台,写尽了英雄们与时代间的相互激荡,造反与政变、背叛与殉道……书中无人不可以为英雄,贩夫走卒、市井小民、娼妇与公主、乞丐与皇帝,莫不可以为英雄。孙晓一次又一次建立英雄的面貌,又一次一次拆解英雄的形象。于穷途末路之时的回眸一笑,是孙晓笔下的安慰与沧桑。', '孙晓');
3、在application.yml文件中配置数据库连接信息和redis连接信息:
spring: ##thymeleaf.# thymeleaf: ##默认前缀 prefix: classpath:/templates/ ##默认后缀 suffix: .html cache: false servlet: content-type: text/html enabled: true encoding: UTF-8 mode: HTML5 ##datasource.# datasource: url: jdbc:mysql://localhost:3306/novel?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource tomcat: max-active: 2 max-wait: 60000 initial-size: 1 min-idle: 1 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: select 'dtouding' test-while-idle: true test-on-borrow: false test-on-return: false ##mybatis.# mybatis: type-aliases-package: com.dtouding.novel.domain configuration: map-underscore-to-camel-case: true default-fetch-size: 100 default-statement-timeout: 30 mapper-locations: classpath:com/dtouding/novel/dao/*.xml redis: host: 127.0.0.1 port: 6379 timeout: 3 password: 123456 poolMaxTotal: 10 poolMaxIdle: 10 poolMaxWait: 3
4、做一个简单的查询小说列表的功能,编写dao、service、controller、html:
public class Novel { private Long id; private String novelName; private String novelCategory; private String novelImg; private String novelSummary; private String novelAuthor; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getNovelName() { return novelName; } public void setNovelName(String novelName) { this.novelName = novelName; } public String getNovelCategory() { return novelCategory; } public void setNovelCategory(String novelCategory) { this.novelCategory = novelCategory; } public String getNovelImg() { return novelImg; } public void setNovelImg(String novelImg) { this.novelImg = novelImg; } public String getNovelSummary() { return novelSummary; } public void setNovelSummary(String novelSummary) { this.novelSummary = novelSummary; } public String getNovelAuthor() { return novelAuthor; } public void setNovelAuthor(String novelAuthor) { this.novelAuthor = novelAuthor; } }
@Mapper public interface NovelDao { @Select("select * from t_novel") List<Novel> list(); }
@Service public class NovelService { @Resource private NovelDao novelDao; public List<Novel> list() { return novelDao.list(); } }
@Controller @RequestMapping(value = "/novel") public class NovelController { @Autowired private NovelService novelService; @RequestMapping(value = "/list", method = RequestMethod.GET) public String list(Model model) { List<Novel> list = novelService.list(); model.addAttribute("novelList", list); return "novel_list"; } }
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>小说列表</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script> <!-- bootstrap --> <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" /> <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script> </head> <body> <div class="panel panel-default"> <div class="panel-heading">小说列表</div> <table class="table" id="goodslist"> <tr><td>小说名称</td><td>小说图片</td><td>小说类别</td><td>小说作者</td><td>小说简介</td> <tr th:each="novel : ${novelList}"> <td th:text="${novel.novelName}"></td> <td ><img th:src="@{${novel.novelImg}}" width="100" height="100" /></td> <td th:text="${novel.novelCategory}"></td> <td th:text="${novel.novelAuthor}"></td> <td th:text="${novel.novelSummary}"></td> </tr> </table> </div> </body> </html>
5、通过http://localhost:8080/novel/list,可访问。
二、缓存novel_list页面
1、引入redis依赖:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
2、编写redis配置类和通用的redis工具类:
@Component @ConfigurationProperties(prefix = "redis") public class RedisConfig { private String host; private int port; private int timeout;//秒 private String password; private int poolMaxTotal; private int poolMaxIdle; private int poolMaxWait;//秒 public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getPoolMaxTotal() { return poolMaxTotal; } public void setPoolMaxTotal(int poolMaxTotal) { this.poolMaxTotal = poolMaxTotal; } public int getPoolMaxIdle() { return poolMaxIdle; } public void setPoolMaxIdle(int poolMaxIdle) { this.poolMaxIdle = poolMaxIdle; } public int getPoolMaxWait() { return poolMaxWait; } public void setPoolMaxWait(int poolMaxWait) { this.poolMaxWait = poolMaxWait; } @Bean public JedisPool jedisPoolFactory() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(poolMaxIdle); jedisPoolConfig.setMaxTotal(poolMaxTotal); jedisPoolConfig.setMaxWaitMillis(poolMaxWait * 1000); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout * 1000, password); return jedisPool; } }
@Service public class RedisService { @Autowired private JedisPool jedisPool; /** * 获取存储对象 * @param key * @param clazz * @param <T> * @return */ public <T> T get(String key, Class<T> clazz) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = jedis.get(key); T t = stringToBean(str, clazz); return t; } finally { returnToPool(jedis); } } /** * 设置对象 * @param key * @param expireSeconds * @param value * @param <T> * @return */ public <T> boolean set(String key, int expireSeconds, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = beanToString(value); if (null == str) { return false; } if (expireSeconds <= 0) { jedis.set(key, str); } else { jedis.setex(key, expireSeconds, str); } return true; } finally { returnToPool(jedis); } } /** * 判断key是否存在 * */ public <T> boolean exists(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.exists(key); }finally { returnToPool(jedis); } } /** * 增加值 * */ public <T> Long incr(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.incr(key); }finally { returnToPool(jedis); } } /** * 减少值 * */ public <T> Long decr(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.decr(key); }finally { returnToPool(jedis); } } private <T> String beanToString(T value) { if (null == value) { return null; } if (value instanceof Integer || value instanceof Long) { return "" + value; } else if (value instanceof String) { return (String) value; } else { return JSON.toJSONString(value); } } private <T> T stringToBean(String str, Class<T> clazz) { if (StringUtils.isEmpty(str) || clazz==null) { return null; } if (clazz==int.class || clazz==Integer.class) { return (T) Integer.valueOf(str); } else if (clazz == String.class) { return (T) str; } else if (clazz==long.class || clazz==Long.class) { return (T)Long.valueOf(str); } else { return JSON.toJavaObject(JSON.parseObject(str), clazz); } } private void returnToPool(Jedis jedis) { if (null != jedis) { jedis.close(); } } }
3、改写NovelController中的list方法,添加页面缓存逻辑,具体包括:
1)、在list方法上添加@ResponseBody注解,并修改返回类型为text/html,可以避免返回的html再次被渲染,因为缓存在redis中的页面是通过代码手工渲染的。
2)、判断redis中是否有novel_list的页面缓存,若有,则直接返回该缓存页面:
String html = redisService.get(NovelRedisKeys.NOVEL_LIST_PAGE, String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
3、若缓存中没有,则借助ThymeleafViewResolver去渲染html页面:
html = thymeleafViewResolver.getTemplateEngine().process("novel_list", webContext);
4、将渲染后的页面缓存到redis中:
if (!StringUtils.isEmpty(html)) { //将渲染后的页面缓存到redis中 redisService.set(NovelRedisKeys.NOVEL_LIST_PAGE, 60, html); }
4、修改后的完整代码如下:
@Controller
@RequestMapping(value = "/novel")
public class NovelController {
@Autowired
private NovelService novelService;
@Autowired
private RedisService redisService;
@Autowired
private ThymeleafViewResolver thymeleafViewResolver;
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model) {
//判断redis是否有缓存
String html = redisService.get(NovelRedisKeys.NOVEL_LIST_PAGE, String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
List<Novel> list = novelService.list();
model.addAttribute("novelList", list);
WebContext webContext = new WebContext(request,
response,
request.getServletContext(),
request.getLocale(),
model.asMap());
//渲染页面
html = thymeleafViewResolver.getTemplateEngine().process("novel_list", webContext);
if (!StringUtils.isEmpty(html)) {
//将渲染后的页面缓存到redis中
redisService.set(NovelRedisKeys.NOVEL_LIST_PAGE, 60, html);
}
return html;
}
}