頁面緩存是應對高併發的一個比較常見的方案,當請求頁面的時候,會先查詢redis緩存中是否存在,若存在則直接從緩存中返回頁面,不然會經過代碼邏輯去渲染頁面,並將渲染後的頁面緩存到redis中,而後返回。下面經過簡單的demo來描述這一過程:javascript
1、準備工做:css
一、新建一個springboot工程,命名爲novel,添加以下依賴:html
<?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>
二、建立一個novel表,並插入幾條數據,以下:java
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', '《英雄志》爲一虛構中國明朝歷史的古典小說,借用明英宗土木堡之變爲背景,以復辟爲舞臺,寫盡了英雄們與時代間的相互激盪,造反與政變、背叛與殉道……書中無人不能夠爲英雄,販夫走卒、市井小民、娼婦與公主、乞丐與皇帝,莫不能夠爲英雄。孫曉一次又一次創建英雄的面貌,又一次一次拆解英雄的形象。於窮途末路之時的回眸一笑,是孫曉筆下的安慰與滄桑。', '孫曉');
三、在application.yml文件中配置數據庫鏈接信息和redis鏈接信息:mysql
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
四、作一個簡單的查詢小說列表的功能,編寫dao、service、controller、html:jquery
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>
五、經過http://localhost:8080/novel/list,可訪問。web
2、緩存novel_list頁面redis
一、引入redis依賴:spring
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
二、編寫redis配置類和通用的redis工具類:sql
@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(); } } }
三、改寫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;
}
三、若緩存中沒有,則藉助ThymeleafViewResolver去渲染html頁面:
html = thymeleafViewResolver.getTemplateEngine().process("novel_list", webContext);
四、將渲染後的頁面緩存到redis中:
if (!StringUtils.isEmpty(html)) { //將渲染後的頁面緩存到redis中 redisService.set(NovelRedisKeys.NOVEL_LIST_PAGE, 60, html); }
四、修改後的完整代碼以下:
@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; }}