Day26: Redis入门、开发点赞功能、开发我收到的赞的功能、重构点赞功能、开发关注、取消关注、开发关注列表、粉丝列表、重构登录功能

Redis入门

简介

  • Redis是NoSQL数据库(Not only SQL)
  • 值支持多种数据结构(key都是string):字符串、哈希、列表、集合、有序集合
  • 把数据存在内存中,速度惊人;
  • 同时也可以讲数据快照(数据备份,定时跑一次)/日志**(AOF,实时存命令)**存在硬盘上,保证数据安全性;
  • Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。

Redis安装

  • mac端使用homebrew进行安装:
cpp 复制代码
brew install redis
  • 安装完成后,你可以使用以下命令来启动Redis服务器:
cpp 复制代码
redis-server /usr/local/etc/redis.conf
  • 你也可以设置Redis作为后台服务运行:
cpp 复制代码
brew services start redis
  • 运行redis客户端:
cpp 复制代码
iris@MateBook ~ % redis-cli
127.0.0.1:6379> select 1
OK

Redis基本使用

  • 相比于python用下划线连接两个单词,redis用冒号:
  • 定位数据库:(默认是0)
cpp 复制代码
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 2
OK
127.0.0.1:6379[2]> select 10
OK
127.0.0.1:6379[10]> select 0
  • 运算strings:
cpp 复制代码
127.0.0.1:6379> set test:count 1 //插入
OK
127.0.0.1:6379> get test:count //查找
"1"
127.0.0.1:6379> incr test:count //加1
(integer) 2
127.0.0.1:6379> get test:count 
"2"
127.0.0.1:6379> decr test:count //减1
(integer) 1
127.0.0.1:6379> get test:count
"1"
  • 运算hashes(hget,hset ):可以理解为strings是key是string,value是单个的hashmap,hashes是key是string,value是hashmap的hashmap。
cpp 复制代码
127.0.0.1:6379> hset test:user id 1 //指明field
(integer) 1
127.0.0.1:6379> hest test:user name zhangsan
(error) ERR unknown command 'hest', with args beginning with: 'test:user' 'name' 'zhangsan' 
127.0.0.1:6379> hset test:user name zhangsan
(integer) 1
127.0.0.1:6379> hget test:user id
"1"
127.0.0.1:6379> hget test:user username //查不到返回nil
(nil)
127.0.0.1:6379> hget test:user name
  • 运算list:相当于一个双向容器,左近左出就是stack,左进右出就是队列。
cpp 复制代码
127.0.0.1:6379> lpush test:ids 101 102 103 //l表示左,从左边依次插入101 102 103
(integer) 3
127.0.0.1:6379> llen test:ids// llen输出list长度
(integer) 3
127.0.0.1:6379> lindex test:ids 0// 0位置对应的值
"103"
127.0.0.1:6379> lindex test:ids 2
"101"
127.0.0.1:6379> lrange test:ids 0 2 //从0-2位置对应的值
1) "103"
2) "102"
3) "101"
127.0.0.1:6379> rpop test:ids //从右边出队,相当于队列
"101"
127.0.0.1:6379> rpop test:ids 
"102"
127.0.0.1:6379> rpop test:ids 
"103"

list作为栈:

cpp 复制代码
127.0.0.1:6379> lpush test:ids 100
(integer) 1
127.0.0.1:6379> lpush test:ids 101
(integer) 2
127.0.0.1:6379> lpush test:ids 102
(integer) 3
127.0.0.1:6379> lpush test:ids 103
(integer) 4
127.0.0.1:6379> lpop test:ids
"103"
127.0.0.1:6379> lpop test:ids
"102"
127.0.0.1:6379> lpop test:ids
"101"
127.0.0.1:6379> lpop test:ids
"100"
  • 运算set:元素无序且不能重复
cpp 复制代码
127.0.0.1:6379> sadd test:teachers aaa vvv bbb cccc ddd //sadd添加元素
(integer) 5
127.0.0.1:6379> scard test:teachers //查找元素数量
(integer) 5
127.0.0.1:6379> spop test:teachers //随机弹出一个元素(可用于抽奖)
"bbb"
127.0.0.1:6379> spop test:teachers
"vvv"
127.0.0.1:6379> scard test:teachers
(integer) 3
127.0.0.1:6379> smembers test:teachers //列出所有元素
1) "aaa"
2) "cccc"
3) "ddd"
  • 运算sorted set:按分数进行排序**(跳表)**
cpp 复制代码
127.0.0.1:6379> zadd test:students 10 aaa 20 bbb 30 ccc 40 ddd 50 eee //值是aaa分数是10
(integer) 5
127.0.0.1:6379> zcard test:students //查找数量
(integer) 5
127.0.0.1:6379> zscore test:students aaa //查找对应值对应的分数
"10"
127.0.0.1:6379> zscore test:students c
(nil)
127.0.0.1:6379> zscore test:students ccc
"30"
127.0.0.1:6379> zrank test:students ccc //查找对应值对应的分数的排名(默认升序)
(integer) 2
127.0.0.1:6379> zrange test:students 0 2 //查找排名在范围内的元素
1) "aaa"
2) "bbb"
3) "ccc"
  • 全局命令
cpp 复制代码
127.0.0.1:6379> keys * //列举所有key
1) "test:students"
2) "test:teachers"
3) "test:user"
4) "test:count"
127.0.0.1:6379> keys test* //列出所有test开头的key
1) "test:students"
2) "test:teachers"
3) "test:user"
4) "test:count"
127.0.0.1:6379> type test:user //列出key对应的value的数据类型
hash
127.0.0.1:6379> type test:ids
none
127.0.0.1:6379> exists test:user //查找是否存在key,存在1,不存在0
(integer) 1
127.0.0.1:6379> exists test:id
(integer) 0
127.0.0.1:6379> del test:user //删除对应的key
(integer) 1
127.0.0.1:6379> exists test:user
(integer) 0
127.0.0.1:6379> expire test:students 10 //为key设置过期时间,单位为s
(integer) 1
127.0.0.1:6379> keys *
1) "test:teachers"
2) "test:count"

整合Redis到Springboot

导包(xml文件)

cpp 复制代码
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
      // <version>3.2.4</version>
  </dependency>
  • 这里可以不置顶版本,maven会自动去父pom中找版本,找不到就用最新版本,我的父pom的版本是3.3.0M1

配置Redis(application.properties)

配置属性文件:

cpp 复制代码
# Redis
spring.data.redis.database = 11
spring.data.redis.port=6379
spring.data.redis.host=localhost

默认端口6379,指定database是哪一号

编写配置类:Configuration/RedisConfig

cpp 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        //序列化的方式(数据转换的方式)
        //设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        
        //设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        
        //设置hashes的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        
        //设置hashes的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());
        
        template.afterPropertiesSet();
        
        return template;
        
        
    }
}
  • 主要是设置redis的序列化方式(就是查到之后我们用什么样的数据类型去接,调用RedisSerializer.string()等工具类;
  • template.afterPropertiesSet(); 执行后触发修改;
  • Redis建立连接需要注入RedisConnectionFactory factory 工厂

访问Redis

编写RedisTests测试类

cpp 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testStrings() {
        String redisKey = "test:count";//相当于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));
    }

    @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"));
    }

    @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));
    }

    @Test
    public void testSets() {
        String redisKey = "test:teachers";

        redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "黄忠");

        System.out.println(redisTemplate.opsForSet().size(redisKey));
        System.out.println(redisTemplate.opsForSet().members(redisKey));
    }

    @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, "八戒"));
        System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
        System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
    }

  @Test
    public void testKeys() {
        redisTemplate.delete("test:count");

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

        redisTemplate.expire("test:user", 10, TimeUnit.SECONDS);
    }
}
  • set->set;
  • lpush->leftPush
  • lpop->leftPop
  • hset->put
  • hget->get...

上面的方法每次都要传入key,会很麻烦,有一个方法是设置绑定变量,然后操作跟之前的一样的:

cpp 复制代码
 //多次访问同一个key,可以减少网络开销
    @Test
    public void testBoundOperations() {
        String redisKey = "test:count";
        //绑定操作
        BoundValueOperations boundValueOperations = redisTemplate.boundValueOps(redisKey);
        boundValueOperations.increment();
        boundValueOperations.increment();
        boundValueOperations.increment();
        boundValueOperations.increment();
        boundValueOperations.increment();
        boundValueOperations.get();


    }

Redis事务

  • 事务管理比较简单;
  • 启动事务后讲命令存在队列中,事务提交后统一批量的进行执行;
  • 如何在Spring中启用,声明式事务不常用,使用编程式事务:
cpp 复制代码
@Test
public void testTransaction() {
    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);
}

这段代码是在Spring Data Redis中使用编程式事务。它使用redisTemplate.execute()方法执行一个SessionCallback,在这个回调中,它启动一个Redis事务,然后执行一系列的操作。
下面是这段代码的详细解释:

  1. redisTemplate.execute(new SessionCallback() {...}):这是Spring Data Redis提供的一种执行Redis操作的方式。SessionCallback是一个回调接口,它的execute方法会在Redis操作执行时被调用。
  2. operations.multi():这行代码启动了一个Redis事务。在事务中,所有的命令都会被序列化和缓存,然后在exec()方法被调用时一次性执行。
  3. operations.opsForSet().add(redisKey, "zhangsan")operations.opsForSet().add(redisKey, "lisi")operations.opsForSet().add(redisKey, "wangwu"):这些代码在Redis的set中添加了三个元素。这些操作都在事务中,所以它们不会立即执行,而是会在exec()方法被调用时执行。
  4. System.out.println(operations.opsForSet().members(redisKey)):这行代码试图打印出set的所有成员。但是因为这个操作也在事务中,所以在exec()被调用之前,它会返回null。
  5. return operations.exec():这行代码执行了事务,这意味着所有在multi()exec()之间的操作都会被一次性执行。exec()方法返回一个List,其中包含了事务中每个操作的结果。
    总的来说,这段代码在一个Redis事务中添加了三个元素到一个set中,然后试图打印出这个set的所有成员,但是因为打印操作也在事务中,所以它会返回null。最后,它执行了事务,并返回了事务的结果。

最后的 输出结果:

  • 本来查询是空的,这是因为处在redis的事务中的查询不会被立即执行;
  • 之后打印出来的内容是obj的结果,也就是事务中每个操作的结果。前面的1,1,1就是插入的时候返回的影响的行数(类似于MySQL)

开发点赞功能

点赞

  • 支持对帖子、评论点赞。
  • 第1次点赞,第2次取消点赞。

首页点赞数量

  • 统计帖子的点赞数量。

详情页点赞数量

  • 统计点赞数量。
  • 显示点赞状态。

因为实时性要求很高,存到redis中,速度快。

数据访问层

(不用写。比较简单,redis类似于操作map,直接集合到业务层)

业务层

写生成key的工具类

因为会生成很多各种各样的key,写一个工具类生成key:

cpp 复制代码
public class RedisKeyUtil {

    private static final String SPLIT = ":";//分隔符

    //存实体的赞
    private static final String PREFIX_ENTITY_LIKE = "like:entity";

    //某个实体的赞
    //key:like:entity:entityType:entityId -> value:集合set(userId),存哪些人点赞了这个实体而不是直接存数字
    public static String getEntityLikeKey(int entityType, int entityId) {
        return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
    } 
}
  • //key:{like:entity:entityType:entityId} -> value:{集合set(userId)},存哪些人点赞了这个实体而不是直接存数字

统计点赞数量

cpp 复制代码
@Service
public class LikeService {
    @Autowired
    private RedisTemplate redisTemplate;

    //点赞
    public void like(int userId, int entityType, int entityId) {//userId是谁点的赞,entityType是点赞的实体类型,entityId是点赞的实体id
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        //判断用户是否已经点过赞
        Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
        if (isMember) {
            redisTemplate.opsForSet().remove(entityLikeKey, userId);//取消点赞
        } else {
            redisTemplate.opsForSet().add(entityLikeKey, userId);//点赞
        }
    }



}

统计查询某实体被点赞的数量

cpp 复制代码
public long findEntityLikeCount(int entityType, int entityId) {
    String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
    return redisTemplate.opsForSet().size(entityLikeKey);
}

查询某人对某实体的点赞状态(某人对某实体是否点过赞)

cpp 复制代码
    public int findEntityLikeStatus(int userId, int entityType, int entityId) {
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
    }

表现层

创建一个新的LikeController:

cpp 复制代码
@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);
        
        
        

    }



}
  • 异步请求,页面不刷新(@RequestMapping注解)
  • 需要把likeCount和likeStatus传给前端。

修改HomeController(首页帖子有多少赞)

cpp 复制代码
if(list != null) {
    for (DiscussPost post : list) {
        Map<String, Object> map = new java.util.HashMap<>();
        map.put("post", post);
        map.put("user", userService.findUserById(post.getUserId()));
        //查询帖子的点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
        map.put("likeCount", likeCount);
        discussPosts.add(map);
    }
}

修改index.html:

cpp 复制代码
<div class="text-muted font-size-12">
    <u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
    <ul class="d-inline float-right">
        <li class="d-inline ml-2">赞 <span th:text="${map.likeCount}">11</span></li>
        <li class="d-inline ml-2">|</li>
        <li class="d-inline ml-2">回帖 <span th:text="${map.post.commentCount}">7</span></li>
    </ul>
</div>

修改帖子详情DiscussPostController

帖子点赞:

cpp 复制代码
....
User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);
//点赞数量
  long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
  model.addAttribute("likeCount",likeCount);
  //点赞状态
  int likeStatus = hostHolder.getUser() == null ? 0 ://未登录默认为未点赞
          likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, post.getId());
  model.addAttribute("likeStatus",likeStatus);

评论点赞:

cpp 复制代码
commentVo.put("user", userService.findUserById(comment.getUserId()));

                // 点赞数量
                likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("likeCount", likeCount);

                // 点赞状态
                likeStatus = hostHolder.getUser() == null ? 0 ://未登录默认为未点赞
                        likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("likeStatus", likeStatus);

评论的评论点赞:

cpp 复制代码
// 回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
// 点赞
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
//点赞状态
likeStatus = hostHolder.getUser() == null ? 0 ://未登录默认为未点赞
        likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());

replyVo.put("likeCount", likeCount);
replyVo.put("likeStatus", likeStatus);

修改DiscussPost.html

cpp 复制代码
 <li class="d-inline ml-2"><a href="javascript:;" th:onclick="|like(this, 1, ${post.id});|" class="text-primary">
                                <b th:text="${likeStatus == 1?'已赞':'赞'}">赞</b>  <i th:text="${likeCount}">111</i>
                            </a>
                            </li>

(评论和楼中楼同理)

开发我收到的赞的功能

累加很麻烦,添加新key比较方便

重构点赞功能

  • 以用户为key,记录点赞数量
  • increment(key),decrement(key)

开发个人主页

  • 以用户为key,查询点赞数量

重构点赞功能

在Util中新添加生成UserKey:

cpp 复制代码
//某个用户的赞
//key:like:user:userId -> value:整数,存这个用户点赞了多少个实体
public static String getUserLikeKey(int userId) {
    return PREFIX_USER_LIKE + SPLIT + userId;
}

重写LikeService的Like函数,将被点赞的人的活动也记录上,而且我们希望活动是不会被打断的,因此需要使用事务:

cpp 复制代码
//点赞
public void like(int userId, int entityType, int entityId, int entityUserId){//userId是谁点的赞,entityType是点赞的实体类型,entityId是点赞的实体id
    //引入事务
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
            String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);//被点赞的用户的key
            //判断用户是否已经点过赞
            boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
            operations.multi();
            if (isMember) {
                operations.opsForSet().remove(entityLikeKey, userId);//取消点赞
                operations.opsForValue().decrement(userLikeKey);//用户赞数减一
            } else {
                operations.opsForSet().add(entityLikeKey, userId);//点赞
                operations.opsForValue().increment(userLikeKey);//用户赞数加一
            }
            return operations.exec();
        }
        //事务之外查询

    });
}

添加查询某用户有多少赞的函数:

cpp 复制代码
    //查询某个用户获得的赞
    public int findUserLikeCount(int userId) {
        String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
        Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
        return count == null ? 0 : count.intValue();
    }
  • 使用intValue把Integer转化为int(开箱)

重构Controller:

cpp 复制代码
@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId){
User user = hostHolder.getUser();

likeService.like(user.getId(), entityType, entityId, entityUserId);//点赞操作

修改discuss-post.html

cpp 复制代码
   th:onclick="|like(this,2,${cvo.comment.id},${cvo.comment.userId});|"

加一个userId

(还要修改discuss.js,在前面已经给出)

开发个人主页

UserController,创建个人主页:

cpp 复制代码
 //个人主页
    @RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
    public String getProfilePage(@PathVariable("userId") int userId, Model model) {
        User user = userService.findUserById(userId);
        if(user == null) {
            throw new RuntimeException("该用户不存在");
        }
        model.addAttribute("user",user);
        int likeCount = likeService.findUserLikeCount(userId);
        model.addAttribute("likeCount",likeCount);
        return "/site/profile";
    }

修改index.html:

cpp 复制代码
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item text-center" th:href="@{|/user/profile/${loginUser.id}|}">个人主页</a>
<a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置</a>
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
<div class="dropdown-divider"></div>

(还有点头像到达的超链接也是同理)

修改profile.html

(就是常规的改header之类的)

最终效果:

开发关注、取消关注

需求

  • 开发关注、取消关注功能。
  • 统计用户的关注数、粉丝数。

关键

  • 若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标)。
  • 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体(不要写死)
  • 为了提高性能,存到Redis

Utils创建key:

cpp 复制代码
//存关注的实体
private static final String PREFIX_FOLLOWEE = "followee";
//存粉丝
private static final String PREFIX_FOLLOWER = "follower";

//某个用户关注的实体
//key:followee:userId:entityType -> value:zset(entityId,now),存这个userId用户关注了哪些实体
public static String getFolloweeKey(int userId, int entityType) {
    return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}

//某个实体的粉丝
//key:follower:entityType:entityId -> value:zset(userId,now),存关注这个实体的用户
public static String getFollowerKey(int entityType, int entityId) {
    return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}

新建FollowService:

cpp 复制代码
@Service
public class FollowService {
    @Autowired
    private RedisTemplate redisTemplate;
    //关注
    public void follow(int userId, int entityType, int entityId) {
        //成对存储,一个存关注的实体,一个存粉丝(事务)
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
                operations.multi();

                operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
                operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

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

    //取消关注
    public void unfollow(int userId, int entityType, int entityId) {
        //成对删除,一个删除关注的实体,一个删除粉丝(事务)
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
                operations.multi();

                operations.opsForZSet().remove(followeeKey, entityId);
                operations.opsForZSet().remove(followerKey, userId);

                return operations.exec();
            }
        });
    }
}
  • 还是用事务,保证原子性;

System.currentTimeMillis() 是 Java 中的一个方法,它返回当前时间(以毫秒为单位)。这个时间是从 1970 年 1 月 1 日 00:00:00 GMT(格林尼治标准时间)开始的毫秒数。这个日期通常被称为 Unix 时间戳或者 Epoch 时间。 例如,如果 System.currentTimeMillis() 返回 1633024800000,那么这表示从 1970 年 1 月 1 日 00:00:00 GMT 到现在已经过去了 1633024800000 毫秒。 这个方法常常被用来测量代码的执行时间,或者生成一个唯一的时间戳。

Follow Controller

cpp 复制代码
@Controller
public class FollowController {
    @Autowired
    private FollowService followService;

    @Autowired
    private HostHolder hostHolder;

    //关注
    @RequestMapping(path = "/follow", method = RequestMethod.POST)//异步的
    @ResponseBody
    public String follow(int entityType, int entityId) {
        User user = hostHolder.getUser();
        followService.follow(user.getId(), entityType, entityId);
        return CommunityUtil.getJsonString(0, "已关注!");
    }

    //取消关注
    @RequestMapping(path = "/unfollow", method = RequestMethod.POST)//异步的
    @ResponseBody
    public String unfollow(int entityType, int entityId) {
        User user = hostHolder.getUser();
        followService.unfollow(user.getId(), entityType, entityId);
        return CommunityUtil.getJsonString(0, "已取消关注!");
    }
}

修改profile.js(关注人的主页)

确保点击关注按钮后,可以将json发送给服务端:

cpp 复制代码
$(function(){
	$(".follow-btn").click(follow);
});

function follow() {
	var btn = this;
	if($(btn).hasClass("btn-info")) {
		// 关注TA
		$.post(
			CONTEXT_PATH + "/follow",
			{"entityType": 3, "entityId":$(btn).prev().val()},
			function(data){
				data = $.parseJSON(data);
				if(data.code == 0) {
					window.location.reload();
				} else {
					alert(data.msg);
				}
			}
		)
		//$(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary");
	} else {
		$.post(
			CONTEXT_PATH + "/unfollow",
			{"entityType": 3, "entityId":$(btn).prev().val()},
			function(data){
				data = $.parseJSON(data);
				if(data.code == 0) {
					window.location.reload();
				} else {
					alert(data.msg);
				}
			}
		)
		// 取消关注
		//$(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
	}
}

显示关注数量

修改service:

cpp 复制代码
 //查询关注目标关注的实体的数量
    public long findFolloweeCount(int userId, int entityType) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
        return redisTemplate.opsForZSet().zCard(followeeKey);
    }

    //查询实体的粉丝数量
    public long findFollowerCount(int entityType, int entityId) {
        String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
        return redisTemplate.opsForZSet().zCard(followerKey);
    }
    
    //查询当前用户是否已关注该实体
    public boolean hasFollowed(int userId, int entityType, int entityId) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
        return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;//查询分数,查不到为null
    }

修改UserController:

cpp 复制代码
@RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
    public String getProfilePage(@PathVariable("userId") int userId, Model model) {
        User user = userService.findUserById(userId);
        if(user == null) {
            throw new RuntimeException("该用户不存在");
        }
        model.addAttribute("user",user);
        int likeCount = likeService.findUserLikeCount(userId);
        model.addAttribute("likeCount",likeCount);

        //查询关注数量
        long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
        model.addAttribute("followeeCount",followeeCount);
        // 查询粉丝数量
        long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
        model.addAttribute("followerCount", followerCount);
        // 查询当前用户是否已关注该实体
        boolean hasFollowed = false;
        if(hostHolder.getUser() != null) {
            hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
        }
        model.addAttribute("hasFollowed", hasFollowed);
        return "/site/profile";
    }

修改profile.html:

cpp 复制代码
<div class="media mt-5">
      <img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle" alt="用户头像" style="width:50px;">
      <div class="media-body">
          <h5 class="mt-0 text-warning">
              <span th:utext="${user.username}">nowcoder</span>
              <input type="hidden" id="userId" th:value="${user.id}" />
              <button type="button" th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
              th:text="${hasFollowed?'已关注':'关注TA'}"
              th:if="${loginUser!= null&&loginUser.id!=user.id}">关注TA</button>
          </h5>
          <div class="text-muted mt-3">
              <span>注册于 <i class="text-muted" th:text="${#dates.format(user.createTime,'yyyy-MM-dd HH:mm:ss')}">2015-06-12 15:20:12</i></span>
          </div>
          <div class="text-muted mt-3 mb-5">
              <span>关注了 <a class="text-primary" href="followee.html" th:text="${followeeCount}">5</a> 人</span>
              <span class="ml-4">关注者 <a class="text-primary" href="follower.html" th:text="${followerCount}">123</a> 人</span>
              <span class="ml-4">获得了 <i class="text-danger" th:text="${likeCount}">87</i> 个赞</span>
          </div>
      </div>
  </div>

开发关注列表、粉丝列表

就是这个功能:

业务层

  • 查询某个用户关注的人,支持分页。
  • 查询某个用户的粉丝,支持分页。

表现层

  • 处理"查询关注的人"、"查询粉丝"请求。
  • 编写"查询关注的人"、"查询粉丝"模板。

Service层

添加查询关注用户和粉丝信息的方法:

cpp 复制代码
//查询用户关注的人的信息
    public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
        if(targetIds == null) {
            return null;
        }
        List<Map<String, Object>> list = new ArrayList<>();
        for(Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }  
        return list;
    }
    //查询某用户的粉丝的信息
    public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
        String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
        //Redis返回的实现类是有序的
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);
        if(targetIds == null) {
            return null;
        }
        List<Map<String, Object>> list = new ArrayList<>();
        for(Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }
        return list;
    }

Controller层

cpp 复制代码
//关注列表
@RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
    User user = userService.findUserById(userId);
    if (user == null) {
        throw new RuntimeException("该用户不存在!");
    }
    //将当前用户传进去是为了xx关注的人填
    model.addAttribute("user", user);

    page.setLimit(5);
    page.setPath("/followees/" + userId);
    //本来查出来用户是long
    page.setRows((int) followService.findFolloweeCount(userId, CommunityConstant.ENTITY_TYPE_USER));//关注列表的实体类型是用户


    //关注列表
    List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
    if (userList != null) {
        for (Map<String, Object> map : userList) {
            User u = (User) map.get("user");
            map.put("hasFollowed", hasFollowed(u.getId()));
        }
    }
    model.addAttribute("users", userList);

    return "/site/followee";
}

//粉丝列表
@RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
    User user = userService.findUserById(userId);
    if (user == null) {
        throw new RuntimeException("该用户不存在!");
    }
    //将当前用户传进去是为了xx关注的人填
    model.addAttribute("user", user);

    page.setLimit(5);
    page.setPath("/followers/" + userId);
    //本来查出来用户是long
    page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));//关注列表的实体类型是用户


    //关注列表
    List<Map<String, Object>> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit());
    if (userList != null) {
        for (Map<String, Object> map : userList) {
            User u = (User) map.get("user");
            map.put("hasFollowed", hasFollowed(u.getId()));
        }
    }
    model.addAttribute("users", userList);

    return "/site/follower";
}

private boolean hasFollowed(int userId) {
    if (hostHolder.getUser() == null) {
        return false;
    }
    return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}

修改profile.html

修改follower.html、followee.html

优化登录模块------redis

使用Redis存储验证码

  • 验证码需要频繁的访问与刷新,对性能要求较高。
  • 验证码不需永久保存,通常在很短的时间后就会失效。
  • 分布式部署时,存在Session共享的问题。

使用Redis存储登录凭证

  • 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。

使用Redis缓存用户信息

  • 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高(还是要存MySQL)。

使用Redis存储验证码

编写创建key的util

cpp 复制代码
//登录验证码
//key:kaptcha:owner -> value:验证码
public static String getKaptchaKey(String owner) {
    return PREFIX_KAPTCHA + SPLIT + owner;
}

重构LoginController的发送验证码方法

cpp 复制代码
public void getKaptcha(HttpServletResponse response, HttpSession session) {
        //生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);
//        //将验证码存入session
//        session.setAttribute("kaptcha", text);
        //验证码的归属
        String kaptchaOwner = CommunityUtil.generateUUID();
        Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
        cookie.setMaxAge(60);//本来由于是session级别的,但是现在是cookie来保存
        cookie.setPath(contextPath);
        response.addCookie(cookie);

        //生成Redis的key
        String kaptchaKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
        //将验证码存入Redis
        redisTemplate.opsForValue().set(kaptchaKey, text, 60, TimeUnit.SECONDS);
        ...
}

重构login的函数,注释掉session

cpp 复制代码
@Value("${server.servlet.context-path}")
    private String contextPath;
    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberme, Model model,  HttpServletResponse response,@CookieValue("kaptchaOwner") String kaptchaOwner) {
        String kaptcha = null;
        //从cookie中获取验证码的归属,看看验证码还在不在
        if(StringUtils.isNotBlank(kaptchaOwner)){
            String kaptchaKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
            kaptcha = (String) redisTemplate.opsForValue().get(kaptchaKey);
        }
...
}
  • @CookieValue注解:从Cookie中取值。

使用Redis存储登录凭证

RedisUtil:

cpp 复制代码
    //登录凭证
       //key:ticket:xxx -> value:User
    public static String getTicketKey(String ticket) {
        return PREFIX_TICKET + SPLIT + ticket;
    }

将loginTicketMapper改为不推荐使用:使用Deprecated注解

cpp 复制代码
@Mapper
//不推荐使用:使用@Deprecated注解
@Deprecated
public interface LoginTicketMapper {

    @Insert({
...}

重构UserService:

cpp 复制代码
@RequestMapping(path = "/login", method = RequestMethod.POST)
    public Map<String, Object> login(String username, String password, int expiredSeconds) {
        Map<String, Object> map = new HashMap<>();
        //空值处理
        if (username == null) {
            map.put("usernameMsg", "用户名不能为空");
            return map;
        }
        if (password == null) {
            map.put("passwordMsg", "密码不能为空");
            return map;
        }
        //验证账号
        User user = userMapper.selectByName(username);
        if (user == null) {
            map.put("usernameMsg", "该用户不存在");
            return map;
        }
        //验证状态
        if (user.getStatus() == 0) {
            map.put("usernameMsg", "该用户未激活");
            return map;
        }
        //验证密码
        password = CommunityUtil.md5(password + user.getSalt());//Salt存在数据库中
        if (!user.getPassword().equals(password)) {
            map.put("passwordMsg", "密码不正确");
            return map;
        }
        //生成登录凭证
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());//ticket是随机字符串
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));

        //
//        loginTicketMapper.insertLoginTicket(loginTicket);
        //将ticket存入Redis
        String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
        //存入Redis(Redis会自动序列化对象)
        redisTemplate.opsForValue().set(redisKey, loginTicket);


        map.put("ticket", loginTicket.getTicket());//最后要把ticket返回给客户端
        return map;
    }

    public void logout(String ticket) {
//        loginTicketMapper.updateStatus(ticket, 1);
        //改为redis的
        String redisKey = RedisKeyUtil.getTicketKey(ticket);
        //取->改->存
        LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
        loginTicket.setStatus(1);
        redisTemplate.opsForValue().set(redisKey, loginTicket);
    }

    public LoginTicket findLoginTicket(String ticket) {
        //改为redis的
        String redisKey = RedisKeyUtil.getTicketKey(ticket);
        return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    }

在 Redis 中,如果在设置键值对时没有显式地指定过期时间,那么这个键值对将会一直存在,直到被显式地删除或者当 Redis 内存不足需要淘汰数据时被自动删除。

使用Redis缓存用户信息

  • 优先从缓存中取值;
  • 取不到初始化缓存数据;
  • 当数据变更时,清除缓存数据。

重构UserService,添加上面说的三个方法:

cpp 复制代码
//从缓存中取用户
    private User getCache(int userId){
        String redisKey = RedisKeyUtil.getUserKey(userId);
        return (User) redisTemplate.opsForValue().get(redisKey);
    }

    //取不到时初始化缓存数据(从MySQL中取)
    private User initCache(int userId){
        User user = userMapper.selectById(userId);
        String redisKey = RedisKeyUtil.getUserKey(userId);
        redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
        return user;
    }

    //数据变更时清除缓存数据
    private void clearCache(int userId){
        String redisKey = RedisKeyUtil.getUserKey(userId);
        redisTemplate.delete(redisKey);
    }

重构UserService中涉及到userMapper.updatexxx的函数:

cpp 复制代码
public User findUserById(int id) {
        //先从缓存中取
        User user = getCache(id);
        if(user == null){
            user = initCache(id);
        }
        return user;
//        return userMapper.selectById(id);
    }

   public int activation(int userId, String code){
        User user = userMapper.selectById(userId);
        if(user.getStatus() == 1){
            return ACTIVATION_REPEAT;
        }else if(user.getActivationCode().equals(code)){
//            userMapper.updateStatus(userId, 1);
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        }else{
            return ACTIVATION_FAILURE;
        }
    }

   public int updateHeader(int userId, String headerUrl) {

//        return userMapper.updateHeader(userId, headerUrl);
        int rows = userMapper.updateHeader(userId, headerUrl);
        clearCache(userId);
        return rows;
    }
相关推荐
水月梦镜花4 小时前
redis:list列表命令和内部编码
数据库·redis·list
掘金-我是哪吒5 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
ketil277 小时前
Ubuntu 安装 redis
redis
王佑辉8 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku0669 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
gorgor在码农9 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王9 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情9 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
minihuabei14 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng16 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust