在上一节STEP9.2 Redis安装中,我们已经把Redis安装好了。本节我们来结合SpringBoot和Redis来做一个简单的项目实践:实现点赞业务。
先讲一下我们的背景:
我们知道“点赞”是现在许多网站、app都支持的功能。尤其是一些大型平台,例如微博,可能一个热帖在一小时内就能飙升几十万甚至几百万的点赞量。
而一次点赞,也就是对服务器的一次修改数据(修改赞数)的请求。而赞数存在我们的MySQL数据库中。那是不是每一次点赞都要操作我们的MySQL数据库,把数据持久化到磁盘呢?如果这样的话,鉴于访问磁盘的速度是很慢的(相较于访问内存而言),我们的访问性能显然达不到理想的预期。
所以我们可以采取这样的方式:
点赞帖子A时:
- 服务收到点赞帖子A的请求
- 服务器检查redis中是否维护了A的点赞数
- redis中维护了A的点赞数,就直接把redis中维护的点赞数+1
- redis中没维护A的点赞数,就先从mysql里把点赞数取出来+1,然后存到redis里(不存回mysql)
查询帖子A的赞数时:
- 服务器收到查询帖子A的赞数的请求
- 服务器检查redis中是否维护了A的点赞数
- redis中维护了A的点赞数,就直接取出来返回
- redis中没维护A的点赞数,就从mysql里把点赞数取出来存进redis,并返回
那么接下来,就让我们尝试写一下代码:
首先做一些必要的准备:
添加依赖,我们需要的依赖有mysql,mybatis,lombok,redis,validation
参考以下pom.xml文件:
<?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 https://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.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jyannis</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</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>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后先来建个数据库posting。在里面建一张表:posting(id,likes),在表里随便放一条数据(1,3)表示一个id=1的帖子,它目前的点赞数是3,如下:
在yml中配置数据库连接信息:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/posting?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: {你的数据库用户名,默认root}
password: {你的数据库密码}
tips:
- 其中posting?后面的一系列参数是关于编码、时区之类的设置,不用纠结。
- 如果你的数据库版本小于5.6,请把driver-class-name里的cj去掉。
接下来建立与表对应的实体:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Posting {
private Integer id;
private Integer likes;
}
构造mapper接口,写我们需要的两个mapper方法:
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface PostingMapper {
@Select("SELECT likes FROM posting WHERE id=#{id}")
Integer getLikesByPrimaryKey(Integer id);
@Update("UPDATE posting SET likes=#{likes} WHERE id=#{id}")
void setLikesByPrimaryKey(@Param("likes")Integer likes,@Param("id")Integer id);
}
然后贴上我们的Redis工具类。我们对于Redis的操作可以基于RedisTemplate来实现,但RedisTemplate的api有些复杂,对于开发者不是很友好,所以我们做进一步封装。
鉴于代码过长,附代码链接:
RedisUtilgitee.com
tips:
- 不需要仔细研读这部分代码,这里只是写一个工具类帮助我们操作redis。通过这个RedisUtil类,我们就可以实现在Redis存储各种数据结构,以及设置过期时间等等。但在点赞业务中,我们只会用到其中最简单的两个api。(set和get)
- 有了这个RedisUtil工具类,大家以后就可以直接调用这里面的方法来非常方便地操作Redis了!而不需要用redisTemplate的什么opsForValue()之类的很麻烦的api。
工具类写完后,为了省事(不是)我们就不写Service接口了,直接把Service类建起来,加上@Service注解,我们来写两个核心方法:
- likes(Integer id)表示给id={id}的帖子点赞
- getLikes(Integer id)表示获取id={id}的帖子的赞数
我们只需要关注其中public方法即可。
import com.jyannis.redisdemo.mapper.PostingMapper;
import com.jyannis.redisdemo.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class LikesService {
@Autowired
PostingMapper postingMapper;
@Autowired
RedisUtil redisUtil;
//redis中存储的key名称规则
private static String LIKE_KEY(Integer id){
return "redisdemo:likes:" + id;
}
//从redis里取出点赞数
private Integer getLikesFromRedis(Integer id){
Integer likes;
try{
//从redis中取出键为LIKE_KEY(Integer id)的值
likes = (Integer)redisUtil.get(LIKE_KEY(id));
}catch (Exception e){
e.printStackTrace();
return null;
}
return likes;
}
public int getLikes(Integer id){
Integer likes = getLikesFromRedis(id);
//如果redis里能取出一个非null值(说明redis有维护这个帖子的点赞数),就直接返回
if(likes != null)return likes;
//redis中没存点赞数的情况
//先从mysql里把点赞数取出来
likes = Optional.ofNullable(postingMapper.getLikesByPrimaryKey(id)).orElse(0);
//存到redis里
redisUtil.set(LIKE_KEY(id),likes);
return likes;
}
public void likes(Integer id){
Integer likes = getLikesFromRedis(id);
//如果redis里能取出一个非null值(说明redis有维护这个帖子的点赞数),就直接在原基础上+1
if(likes != null){
redisUtil.set(LIKE_KEY(id),likes + 1);
}else{
//redis取不出,就从mysql取
likes = Optional.ofNullable(postingMapper.getLikesByPrimaryKey(id)).orElse(0);
//存到redis里
redisUtil.set(LIKE_KEY(id),likes + 1);
}
}
}
其实这里也就是实现了本文一开始讲的两个逻辑(点赞帖子和查询帖子赞数)。如果读者淡忘了,可以往上翻一翻。(不过其实这里注释也写的非常清楚了)
这里再补充几个小tip:
静态方法LIKE_KEY(Integer id)的含义是,为id={id}的帖子生成它对应的点赞数的键key。毕竟Redis是基于键值对存储的嘛,没有键哪里来的值对吧。
getLikesFromRedis(Integer id)方法里有个try-catch语句块,这是为了在redis连接失败时能返回null,而不是直接导致程序无法继续运行下去。
读者可能对Optional.ofNullable(Object object1).orElse(Object object2)的写法有些疑惑,含义其实就是:如果object1不为null,就返回object1;如果object1为null,就返回object2。
例如下面这行代码:
likes = Optional.ofNullable(postingMapper.getLikesByPrimaryKey(id)).orElse(0);
含义就是:
如果postingMapper.getLikesByPrimaryKey(id)取出来的likes不是null,就直接赋给likes变量。如果是null,就likes = 0。
最后来写一下我们的controller代码,这段就非常简单了:
import com.jyannis.redisdemo.service.LikesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/likes")
@Validated
public class LikesController {
@Autowired
LikesService likesService;
@GetMapping("/{id}")
public int getLikes(@NotNull @PathVariable Integer id){
return likesService.getLikes(id);
}
@PostMapping("/{id}")
public int likes(@NotNull @PathVariable Integer id){
likesService.likes(id);
return 0;
}
}
相信里面大部分代码读者都很熟悉了。如果对@NotNull和@Validated不熟悉,那你可能需要复习一下这个:
STEP6.4 参数校验zhuanlan.zhihu.com
那么到以上部分为止,其实关于redis的使用读者已经初窥门径了。代码整理如下:
redis-demogitee.com
我们先来做个简单的测试:
(注意,测试前请先给自己数据库的posting表插入一条id=1,likes=3的数据!)
启动我们的redis服务:(怎么启动?STEP9.2 Redis安装)
同时也启动我们的项目。
然后在浏览器地址栏访问:http://localhost:8080/likes/1
可以看到获取到了点赞数是3。我们现在通过postman连续发三次post请求如下:
然后再来http://localhost:8080/likes/1:
可以看到点赞3次后,3变成了6。
那么这个时候,我们把redis宕机会怎么样呢?
我们来把redis的窗口关掉:
然后再来访问一次http://localhost:8080/likes/1:
tips:这次可能访问比较慢,因为服务器发现连不上redis了,就不断重试,直到超时(默认1分钟)
我们发现6丢失了,只能访问mysql获取到3。
到这里也就顺带证明了,redis是基于内存的数据库,是具有易失性的。
接下来的部分就和redis无关了,如果不感兴趣可以跳过,不会影响我们系列课程之后的内容。
接下来,我们完善一下我们的点赞业务:
我们帖子的点赞数应当是要定期存回mysql去的。那么应该怎么设计和实现呢?
很简单,为我们的项目开设一个定时任务,在每天“用户访问比较少,服务器压力很小”的时候,把点赞数刷回磁盘。姑且就定在每晚十二点吧!
那么这么写这个代码呢?
先在我们的启动类RedisDemoApplication上加一个@EnableScheduling注解,表示“开启定时任务”。
@SpringBootApplication
@EnableScheduling
public class RedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
}
然后来写一个定时任务。为了便于测试,我们先不写“每晚十二点执行”的定时任务,而是写一个“每2秒执行一次”的定时任务:
import com.jyannis.redisdemo.mapper.PostingMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTasks {
@Autowired
RedisUtil redisUtil;
@Autowired
PostingMapper postingMapper;
//每隔2s执行一次
@Scheduled(fixedRate = 2000)
public void reportCurrentTime() {
Integer likes;
try{
//从redis中取出键为LIKE_KEY(Integer id)的值
likes = (Integer)redisUtil.get("redisdemo:likes:1");
if(likes != null){
postingMapper.setLikesByPrimaryKey(likes,1);
}
}catch (Exception e){
}
}
}
并且为了写起来方便,我们这里只针对id=1的帖子做一下定期刷盘操作。我们可以简单测试一下。
开启redis服务,访问几次点赞操作,然后过两秒查一下数据库,会发现mysql里数据也得到了更新。
那么怎么把每隔两秒变成每天十二点执行呢?只需要把@Schedule的参数改一下:
@Scheduled(cron = "0 0 0 * * ?")
最后再附一次项目链接:
redis-demogitee.com