在上一节STEP9.2 Redis安装中,我们已经把Redis安装好了。本节我们来结合SpringBoot和Redis来做一个简单的项目实践:实现点赞业务。

先讲一下我们的背景:

我们知道“点赞”是现在许多网站、app都支持的功能。尤其是一些大型平台,例如微博,可能一个热帖在一小时内就能飙升几十万甚至几百万的点赞量。

而一次点赞,也就是对服务器的一次修改数据(修改赞数)的请求。而赞数存在我们的MySQL数据库中。那是不是每一次点赞都要操作我们的MySQL数据库,把数据持久化到磁盘呢?如果这样的话,鉴于访问磁盘的速度是很慢的(相较于访问内存而言),我们的访问性能显然达不到理想的预期。

所以我们可以采取这样的方式:

点赞帖子A时

  1. 服务收到点赞帖子A的请求
  2. 服务器检查redis中是否维护了A的点赞数
  1. redis中维护了A的点赞数,就直接把redis中维护的点赞数+1
  2. redis中没维护A的点赞数,就先从mysql里把点赞数取出来+1,然后存到redis里(不存回mysql)

查询帖子A的赞数时

  1. 服务器收到查询帖子A的赞数的请求
  2. 服务器检查redis中是否维护了A的点赞数
  1. redis中维护了A的点赞数,就直接取出来返回
  2. 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,如下:




点赞redis实现 redis实现点赞功能_mysql


在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:

  1. 其中posting?后面的一系列参数是关于编码、时区之类的设置,不用纠结。
  2. 如果你的数据库版本小于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:

  1. 不需要仔细研读这部分代码,这里只是写一个工具类帮助我们操作redis。通过这个RedisUtil类,我们就可以实现在Redis存储各种数据结构,以及设置过期时间等等。但在点赞业务中,我们只会用到其中最简单的两个api。(set和get)
  2. 有了这个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

点赞redis实现 redis实现点赞功能_redis工具类_02


我们先来做个简单的测试:

(注意,测试前请先给自己数据库的posting表插入一条id=1,likes=3的数据!)

启动我们的redis服务:(怎么启动?STEP9.2 Redis安装)


点赞redis实现 redis实现点赞功能_点赞redis实现_03


同时也启动我们的项目。

然后在浏览器地址栏访问:http://localhost:8080/likes/1


点赞redis实现 redis实现点赞功能_redis_04


可以看到获取到了点赞数是3。我们现在通过postman连续发三次post请求如下:


点赞redis实现 redis实现点赞功能_redis_05


然后再来http://localhost:8080/likes/1:


点赞redis实现 redis实现点赞功能_点赞redis实现_06


可以看到点赞3次后,3变成了6。

那么这个时候,我们把redis宕机会怎么样呢?

我们来把redis的窗口关掉:


点赞redis实现 redis实现点赞功能_mysql_07


然后再来访问一次http://localhost:8080/likes/1:

tips:这次可能访问比较慢,因为服务器发现连不上redis了,就不断重试,直到超时(默认1分钟)


点赞redis实现 redis实现点赞功能_redis工具类_08


我们发现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

点赞redis实现 redis实现点赞功能_redis工具类_02