4.1 Redis入门

redisserializer性能对比_redisserializer性能对比

https://redis.io

redis官网只提供了Linux环境下的安装包,没有提供针对windows的安装包,但是微软提供了针对windows环境下的redis安装包。 https://github.com/MicrosoftArchive/redis/releases

windows下的redis安装包各个版本放到了阿里云盘,自取:

redis-for-windows

提取码: 80im


安装完成之后:

redisserializer性能对比_缓存_02

在windows下我们访问redis客户端通常会通过命令行的方式访问,为了方便访问,我们把安装路径配到环境变量里,这样命令行就可以直接访问到这些工具了。

redisserializer性能对比_redis_03

redisserializer性能对比_缓存_04

redis安装完成之后服务自动启动了,接下来我们来启用一下redis的客户端

使用命令 redis-cli

redisserializer性能对比_redisserializer性能对比_05

redis提倡如果一个名字由两个单次构成,建议中间加上 冒号

redis操作相关数据的命令可以看我以前写的博客:

Redis指定配置文件启动、数据库相关指令以及Redis操作String类型、List类型

Redis操作Set、Zset、Hash数据类型以及可视化工具的使用

相关指令

# 1.DEL指令
- 语法 :  DEL key (如果要删除多个key,不同key之间可以用空格隔开,例如: DEL key1 key2 key3) 
- 作用 :  删除给定的一个或多个key 。不存在的key 会被忽略。
- 可用版本: >= 1.0.0
- 返回值: 被删除key 的数量。 

# 2.EXISTS指令
- 语法:  EXISTS key	(如果要判断多个key 不同key之间用空格隔开 例如:EXISTS key1 key2 key3 只要存在1										 个,就返回存在的数量)
- 作用:  检查给定key 是否存在。
- 可用版本: >= 1.0.0
- 返回值: 若key 存在,返回1 ,否则返回0。

# 3.EXPIRE
- 语法:  EXPIRE key seconds
- 作用:  为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
- 可用版本: >= 1.0.0
- 时间复杂度: O(1)
- 返回值:设置成功返回1 。

# 4.KEYS
- 语法 :  KEYS pattern
- 作用 :  查找所有符合给定模式pattern 的key 。
- 语法:
	KEYS * 匹配数据库中所有key 。
	KEYS h?llo 匹配hello ,hallo 和hxllo 等。	(?代表一个任意字符,不能代表0个字符)
	KEYS h*llo 匹配hllo 和heeeeello 等。			 (*代表任意多个字符,可以是0个)
	KEYS h[ae]llo 匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开([]也是只能匹配一个,只能匹						配hallo或者是hello,如果想要匹配多个,可以使用h[ae][ae]llo )
- 可用版本: >= 1.0.0
- 返回值: 符合给定模式的key 列表。

# 5.MOVE
- 语法 :  MOVE key db
- 作用 :  将当前数据库的key 移动到给定的数据库db 当中。
- 可用版本: >= 1.0.0
- 返回值: 移动成功返回1 ,失败则返回0 。

# 6.PEXPIRE
- 语法 :  PEXPIRE key milliseconds
- 作用 :  这个命令和EXPIRE 命令的作用类似,但是它以毫秒为单位设置key 的生存时间,而不像EXPIRE 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 时间复杂度: O(1)
- 返回值:设置成功,返回1  key 不存在或设置失败,返回0

# 7.PEXPIREAT
- 语法 :  PEXPIREAT key milliseconds-timestamp
- 作用 :  这个命令和EXPIREAT 命令类似,但它以毫秒为单位设置key 的过期unix 时间戳,而不是像EXPIREAT那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值:如果生存时间设置成功,返回1 。当key 不存在或没办法设置生存时间时,返回0 。(查看EXPIRE 命令获取更多信息)

# 8.TTL
- 语法 :   TTL key
- 作用 :   以秒为单位,返回给定key 的剩余生存时间(TTL, time to live)。
- 可用版本: >= 1.0.0
- 返回值:
	当key 不存在时,返回-2 。
	当key 存在但没有设置剩余生存时间时,返回-1 。
	否则,以秒为单位,返回key 的剩余生存时间。
- Note : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。

# 9.PTTL
- 语法 :  PTTL key
- 作用 :  这个命令类似于TTL 命令,但它以毫秒为单位返回key 的剩余生存时间,而不是像TTL 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值: 当key 不存在时,返回-2 。当key 存在但没有设置剩余生存时间时(即永久存储),返回-1 。
- 否则,以毫秒为单位,返回key 的剩余生存时间。
- 注意 : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。

# 10.RANDOMKEY
- 语法 :  RANDOMKEY
- 作用 :  从当前数据库中随机返回(不删除) 一个key 。
- 可用版本: >= 1.0.0
- 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil 。

# 11.RENAME
- 语法 :  RENAME key newkey
- 作用 :  将key 改名为newkey 。当key 和newkey 相同,或者key 不存在时,返回一个错误。当newkey 已经存在时,RENAME 命令将覆盖旧值。
- 可用版本: >= 1.0.0
- 返回值: 改名成功时提示OK ,失败时候返回一个错误。

# 12.TYPE
- 语法 :  TYPE key
- 作用 :  返回key 所储存的值(value)的类型。
- 可用版本: >= 1.0.0
- 返回值:
	none (key 不存在)
	string (字符串)
	list (列表)
	set (集合)
	zset (有序集)
	hash (哈希表)
Redis操作String类型数据
1. 存存储模型

redisserializer性能对比_数据库_06

2. 常用操作命令

命令

说明

set

设置一个key/value

get

根据key获得对应的value

mset

一次设置多个key value

mget

一次获得多个key的value

getset

获得原始key对应的value,同时设置新的value

strlen

获得指定key对应的value的长度

append

为key对应的value追加内容并返回追加之后value的长度

getrange

获取指定key对应的value指定索引范围的内容

setex

在设置一个key的时候就指定存活的有效期(单位: 秒)

psetex

在设置一个key的时候就指定存活的有效期(单位: 毫秒)

setnx

存在key时不做任何操作,不存在添加

msetnx 原子操作(只要有一个存在不做任何操作)

可以同时设置多个key,只有有一个存在都不保存

decr

若key对应的value为数值类型(数值串)对value进行-1操作

decrby

若key对应的value为数值类型(数值串)对value减去指定数值

Incr

对key对应的value数值类型(数值串)进行+1操作

incrby

对key对应的value数值类型(数值串)加上指定数值

Incrbyfloat

对key对应的value数值类型(数值串)加上浮点数

Redis操作List类型数据
1.内存存储模型

redisserializer性能对比_后端_07

2.常用操作指令

命令

说明

lpush

存在列表就直接放,不存在就先创建列表再放,将某个值(某些值)加入到key列表头部

lpushx

同lpush,但是必须要保证这个key存在

rpush

存在列表就直接放,不存在就先创建列表再放,将某个值(某些值)加入到key列表尾部

rpushx

同rpush,但是必须要保证这个key存在

lpop

返回和移除列表首部的第一个元素

rpop

返回和移除列表尾部的第一个元素

lrange

获取key对应的列表指定下标区间内的元素

llen

获取key对应的列表元素个数

lset

设置key对应的列表某一个指定索引的值(索引必须存在)

lindex

获取key对应的列表某一个指定索引位置的元素

lrem

从key对应的列表中从左到右删除指定数量的对应元素

ltrim

只保留列表中特定区间内的元素,删除其他元素

linsert

在某一个元素之前,之后插入新元素

Redis操作Set类型数据
1.内存存储模型

redisserializer性能对比_redisserializer性能对比_08

2.常用操作指令

命令

说明

sadd

没有Set时创建,之后向key对应的类型为Set的value添加元素

smembers

显示key对应的Set集合中所有元素 (无序)

scard

返回key对应的Set集合中元素的个数

spop

随机返回一个(多个)元素 并将元素在Set集合中删除

smove

从一个Set集合中向另一个Set集合移动元素 必须是同一种类型

srem

从key对应的Set集合中删除指定的一个(多个)元素

sismember

判断key对应的Set集合中是否含有这个元素

srandmember

随机返回key对应的Set集合中的一个(多个)元素

sdiff

展示去掉第一个集合中其它集合含有的相同元素(仅仅只是展示,在数据库中并没有实际删除)

sinter

求交集

sunion

求和集

Redis操作Zset类型数据
1.内存模型

redisserializer性能对比_后端_09

2.常用命令

命令

说明

zadd

没有Zset时先创建,添加一个有序集合元素

zcard

返回集合的元素个数

zrange 升序 zrevrange 降序

返回升序|降序的元素列表,Zset根据得分进行排序

zrangebyscore

按照分数查找一个范围内的元素

zrank

返回排名

zrevrank

倒序排名

zscore

显示某一个元素的分数

zrem

移除某一个元素

zincrby

给某个特定元素加分

Redis操作Hash类型数据
1. 内存模型

redisserializer性能对比_数据库_10

2.常用命令

命令

说明

hset

设置一个key为String,value为key-value的键值对

hget

获得一个key对应的value

hgetall

获得所有的key/value对

hdel

删除某一个key/value对

hexists

判断一个key是否存在

hkeys

获得所有的key

hvals

获得所有的value

hmset

设置多个key/value

hmget

获得多个key的value

hsetnx

设置一个不存在的key的值

hincrby

为value进行加法运算

hincrbyfloat

为value加入浮点值

4.2 Spring整合Redis

redisserializer性能对比_缓存_11

1.引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.配置Redis

application.properties 配置文件中配置 redis 相关的

# RedisProperties 配置redis相关的(使用redis中的哪个数据库、redis所在服务器的ip、redis服务器的端口)
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379

redisserializer性能对比_后端_12

编写配置类,构造RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 方法参数 RedisConnectionFactory 会自动被工厂注入
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 通过链接工厂创建链接
        template.setConnectionFactory(factory);

        // 设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());

        template.afterPropertiesSet();
        return template;
    }
}

redisserializer性能对比_缓存_13

3.访问Redis

@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testStrings(){
        String redisKey = "test:count";

        redisTemplate.opsForValue().set(redisKey, 1);

        System.out.println(redisTemplate.opsForValue().get(redisKey));
        System.out.println(redisTemplate.opsForValue().increment(redisKey));
        System.out.println(redisTemplate.opsForValue().decrement(redisKey));
    }
    /*
    结果:
        1
        2
        1
    */
    @Test
    public void testHashes() {
        String redisKey = "test:user";

        redisTemplate.opsForHash().put(redisKey, "id", 1);
        redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");

        System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
        System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
    }
    /*
    结果:
        1
        zhangsan
    */
    @Test
    public void testLists() {
        String redisKey = "test:ids";

        redisTemplate.opsForList().leftPush(redisKey, 101);
        redisTemplate.opsForList().leftPush(redisKey, 102);
        redisTemplate.opsForList().leftPush(redisKey, 103);

        System.out.println(redisTemplate.opsForList().size(redisKey));
        System.out.println(redisTemplate.opsForList().index(redisKey, 0));
        System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));

        System.out.println(redisTemplate.opsForList().leftPop(redisKey));
        System.out.println(redisTemplate.opsForList().leftPop(redisKey));
        System.out.println(redisTemplate.opsForList().leftPop(redisKey));
    }
    /*
    结果:
        3
        103
        [103, 102, 101]
        103
        102
        101
    */
    @Test
    public void testSets() {
        String redisKey = "test:teachers";

        redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");

        System.out.println(redisTemplate.opsForSet().size(redisKey));
        System.out.println(redisTemplate.opsForSet().pop(redisKey));
        System.out.println(redisTemplate.opsForSet().members(redisKey));
    }
    /*
    结果:
        5
        诸葛亮
        [张飞, 关羽, 赵云, 刘备]
    */
    @Test
    public void testSortedSets() {
        String redisKey = "test:students";

        redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
        redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
        redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
        redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
        redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);

        System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
        System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
        // reverseRank:由大到小     rank:由小到大
        System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
        System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
    }
    /*
    结果:
        5
        50.0
        4
        [悟空, 唐僧, 沙僧]
    */
    @Test
    public void testKeys() {
        redisTemplate.delete("test:user");

        System.out.println(redisTemplate.hasKey("test:user"));

        redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
    }
    /*
    结果:
        false
    */


    /**
     * 有可能每次访问的时候都访问同一个key,每个api都要把这个key传进去,
     * 有点麻烦,针对这样的逻辑我们可以绑定这个key,每次操作都一定是针对
     * 这个key的,不用传key了。
     */
    // 多次访问同一个key的话我们可以绑定key
    @Test
    public void testBoundOperations() {
        String redisKey = "test:count";
        BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
        operations.increment();
        operations.increment();
        operations.increment();
        operations.increment();
        operations.increment();
        System.out.println(operations.get());
    }
    /*
    结果:
        6
    */

    // 编程式事务
    @Test
    public void testTransactional() {
        Object obj = redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String redisKey = "test:tx";

                operations.multi();         // 启用事务

                operations.opsForSet().add(redisKey, "zhangsan");
                operations.opsForSet().add(redisKey, "lisi");
                operations.opsForSet().add(redisKey, "wangwu");

                System.out.println(operations.opsForSet().members(redisKey));

                return operations.exec();   // 提交事务
            }
        });
        System.out.println(obj);
    }
    /*
    结果:
        []
        [1, 1, 1, [zhangsan, wangwu, lisi]]
     */
}

重点:redis中事务

因为redis是一个数据库,它也是支持事务的,但是它所支持的事务的机制不完全满足ACID四个特性,因为毕竟它不是关系型数据库,只有关系型数据库才严格满足这四个特点,整体来说redis的事务管理是比较简单的。它的机制是:当我启用事务以后,当我再去执行一个redis命令的时候,它并不会立刻执行这个命令,而是把这个命令放到一个队列里先存着,然后再执行一个命令再放到队列里,直到提交事务的时候,它会把队列中的命令一股脑发给redis服务器一起执行。这里有一个隐含的问题需要注意:因为事务之内的命令不会立刻执行,而是提交时统一批量的执行,所以如果在事务的过程中做了一个查询(这个查询不受事务管理),这个查询不会立刻返回结果,也就是说不要在事务中间去做查询,要么提交查,要么事务提交之后查!!!

Spring支持redis的声明式事务和编程式事务,也是声明式事务更简单,只要做一些配置,加上@Transactional注解就可以了,但是因为redis的事务有刚才所说的问题的存在,所以我们通常不会用声明式事务,因为声明式事务只能精确到一个方法,在方法上加上@Transactional,方法内部整个逻辑都是整个的事务范围,这个方法之内就没办法去查询了。所以我们通常都用编程式事务把事务的范围缩小。

编程式事务格式:

@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;
    // 编程式事务
  
    @Test
    public void testTransactional() {
        Object obj = redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String redisKey = "test:tx";

                operations.multi();         // 启用事务

                operations.opsForSet().add(redisKey, "zhangsan");
                operations.opsForSet().add(redisKey, "lisi");
                operations.opsForSet().add(redisKey, "wangwu");

                System.out.println(operations.opsForSet().members(redisKey));

                return operations.exec();   // 提交事务
            }
        });
        System.out.println(obj);
    }
    /*
    结果:
        []
        [1, 1, 1, [zhangsan, wangwu, lisi]]
     */
}

4.3 点赞

redisserializer性能对比_redis_14

因为点赞频率非常高,如果把数据存到硬盘里,读写性能很差,所以我们把数据存到redis里,存到内存里性能非常好,还可以使用redis的持久化机制将数据从内存存到硬盘里。


因为之前开发的时候是存到硬盘里,开发顺序 dao -> service -> controller,但是因为我们这次是存到redis里,直接写redis命令就可以,所以我们数据访问层不开发了,直接将redis命令写到service层里。

首先我们写一个工具类专门生成redis的key,好复用这个工具类RedisKeyUtil。

配置 RedisTemplate 和 书写工具类生成redis的key

配置 RedisTemplate 之前已写过,所以可以不用写了。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 方法参数 RedisConnectionFactory 会自动被工厂注入
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 通过链接工厂创建链接
        template.setConnectionFactory(factory);

        // 设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());

        template.afterPropertiesSet();
        return template;
    }
}

写一个工具类去生成redis的key

public class RedisKeyUtil {

    private static final String SPLIT = ":";                            // key中间的 :
    private static final String PREFIX_ENTITY_LIKE = "like:entity";     // key的前缀
    
    // 某个实体的赞
    // like:entity:entityType:entityId -> set(userId)
    // 我们最终要把这个点赞的userId存到set里,因为我们未来可能会开发查看谁给我点赞的功能
    public static String getEntityLikeKey(int entityType, int entityId) {
        return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
    }

}

redisserializer性能对比_后端_15

开发 Service 层

@Service
public class LikeService {

    @Autowired
    private RedisTemplate redisTemplate;

    // 点赞
    public void like(int userId, int entityType, int entityId, int entityUserId) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);

                boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);

                operations.multi();

                if (isMember) {
                    // 点过赞再点赞就是取消赞
                    operations.opsForSet().remove(entityLikeKey, userId);
                } else {
                    // 没过点赞的话就是点赞
                    operations.opsForSet().add(entityLikeKey, userId);
                }

                return operations.exec();
            }
        });
    }

    // 查询某实体(帖子被点赞)点赞的数量
    public long findEntityLikeCount(int entityType, int entityId) {
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        return redisTemplate.opsForSet().size(entityLikeKey);
    }

    // 查询某人对某实体的点赞状态
    public int findEntityLikeStatus(int userId, int entityType, int entityId) {
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
    }
}

redisserializer性能对比_redisserializer性能对比_16

开发 表现层

这个“点赞”是一个异步请求,所以方法上也要加上 @ResponseBody 注解

@Controller
public class LikeController {

    @Autowired
    private LikeService likeService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/like", method = RequestMethod.POST)
    @ResponseBody
    public String like(int entityType, int entityId){
        User user = hostHolder.getUser();
        // 点赞
        likeService.like(user.getId(), entityType, entityId);
        // 数量
        Long likeCount = likeService.findEntityLikeCount(entityType, entityId);
        // 状态
        int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
        // 返回的结果
        Map<String, Object> map = new HashMap<>();
        map.put("likeCount", likeCount);
        map.put("likeStatus", likeStatus);

        return CommunityUtil.getJSONString(0, null, map);
    }
}

redisserializer性能对比_数据库_17

点赞是在帖子详情页面点的,所以我们需要打开帖子详情页面discuss-detail.html做一个处理:

因为是异步请求,所以我们还需要提供一个js方法来实现点赞提交请求的逻辑。

discuss-detail.html

redisserializer性能对比_redisserializer性能对比_18

redisserializer性能对比_缓存_19

redisserializer性能对比_后端_20

redisserializer性能对比_数据库_21

redisserializer性能对比_缓存_22

我们还需要对首页index.html帖子显示的赞的数量做一个处理

首先是返回首页请求的HomeController

redisserializer性能对比_缓存_23

然后是 index.html

redisserializer性能对比_后端_24


然后需要对帖子详情页面显示的赞的数量做一个处理(加一些内容)

DiscussPostController

redisserializer性能对比_数据库_25

redisserializer性能对比_缓存_26

redisserializer性能对比_数据库_27

redisserializer性能对比_redis_28

然后是详情页面discuss-detail.html

redisserializer性能对比_缓存_29

redisserializer性能对比_后端_30

redisserializer性能对比_后端_31

4.4 我收到的赞

redisserializer性能对比_后端_32

重构点赞功能

这里我们处理用户获得的点赞数量,我们可以在点赞的时候把这个赞的数量存到redis里(以用户id为key),所以我们需要重构一下以前写的点赞的代码

RedisKeyUtil工具类里写一个新方法设置user存到redis里的key设置成什么

redisserializer性能对比_数据库_33

首先是LikeService

redisserializer性能对比_后端_34

redisserializer性能对比_后端_35

然后是LikeController

redisserializer性能对比_后端_36

然后是 discuss-detail.html

redisserializer性能对比_redisserializer性能对比_37

redisserializer性能对比_redisserializer性能对比_38

redisserializer性能对比_redis_39

redisserializer性能对比_后端_40

开发展示个人主页功能

直接把功能写在 UserController

redisserializer性能对比_redisserializer性能对比_41

redisserializer性能对比_数据库_42

然后是“个人主页”页面profile.html

redisserializer性能对比_redisserializer性能对比_43

redisserializer性能对比_后端_44

redisserializer性能对比_数据库_45

然后在首页index.html上加上用户头像的访问路径

redisserializer性能对比_数据库_46

帖子详情页discuss-detail.html改头像的链接使其跳转到个人主页

redisserializer性能对比_数据库_47

redisserializer性能对比_redis_48

私信页面letter.html改头像的链接使其跳转到展示个人主页的controller

redisserializer性能对比_数据库_49

私信详情页面letter-detail.html改头像的链接使其跳转到展示个人主页的controller

redisserializer性能对比_缓存_50

最后启动项目之后,经自己测试开发成功。