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

Redis入门

简介

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

Redis安装

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

Redis基本使用

  • 相比于python用下划线连接两个单词,redis用冒号:
  • 定位数据库:(默认是0)
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:
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。
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,左进右出就是队列。
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作为栈:

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:元素无序且不能重复
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:按分数进行排序**(跳表)**
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"

  • 全局命令
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文件)

<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)

配置属性文件:

# Redis
spring.data.redis.database = 11
spring.data.redis.port=6379
spring.data.redis.host=localhost

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

编写配置类:Configuration/RedisConfig

@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测试类

image

@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,会很麻烦,有一个方法是设置绑定变量,然后操作跟之前的一样的:

 //多次访问同一个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中启用,声明式事务不常用,使用编程式事务:
@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。最后,它执行了事务,并返回了事务的结果。

最后的 输出结果:

image

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

开发点赞功能

点赞

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

首页点赞数量

  • 统计帖子的点赞数量。

详情页点赞数量

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

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

数据访问层

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

业务层

写生成key的工具类

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

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)},存哪些人点赞了这个实体而不是直接存数字

统计点赞数量

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



}

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

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

表现层

创建一个新的LikeController:

@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(首页帖子有多少赞)

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:

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

image

修改帖子详情DiscussPostController

帖子点赞:

....
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);

评论点赞:

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

评论的评论点赞:

// 回复目标
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

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

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

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

//点赞
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();
        }
        //事务之外查询

    });
}

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

    //查询某个用户获得的赞
    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:

@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

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

加一个userId

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

开发个人主页

UserController,创建个人主页:

 //个人主页
    @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:

<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之类的)

最终效果:

image

开发关注、取消关注

需求

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

关键

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

Utils创建key:

//存关注的实体
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:

@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

@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发送给服务端:

$(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:

 //查询关注目标关注的实体的数量
    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:

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

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

image

开发关注列表、粉丝列表

就是这个功能:

image

业务层

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

表现层

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

Service层

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

//查询用户关注的人的信息
    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层

//关注列表
@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

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

重构LoginController的发送验证码方法

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

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

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

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

@Mapper
//不推荐使用:使用@Deprecated注解
@Deprecated
public interface LoginTicketMapper {

    @Insert({
...}

重构UserService:

@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,添加上面说的三个方法:

//从缓存中取用户
    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的函数:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/570546.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(一)JVM实战——jvm的组成部分详解

前言 本节内容是关于java虚拟机JVM组成部分的介绍&#xff0c;通过其组成架构图了解JVM的主要组成部分。 正文 ClassFile&#xff1a;字节码文件 - javac&#xff1a;javac前端编译器将源代码编译成符合jvm规范的.class文件&#xff0c;即字节码文件 - class文件的结构组成&a…

【智能算法】指数分布优化算法(EDO)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2023年&#xff0c;M Abdel-Basset等人受到指数分布理论启发&#xff0c;提出了指数分布优化算法&#xff08;Exponential Distribution Optimizer, EDO&#xff09;。 2.算法原理 2.1算法思想 ED…

mac系统镜像源管理之nrm的安装与使用

之前有介绍过&#xff1a;pnpm安装和使用&#xff0c;nvm安装及使用&#xff0c;在前端开发中其实还有一个工具也会偶尔用到&#xff0c;那就是nrm&#xff0c;本文就详解介绍一下这个工具&#xff0c;非常的简单且好用&#xff5e; 文章目录 1、什么是nrm&#xff1f;2、安装3…

CPU资源控制

一、CPU资源控制定义 cgroups&#xff08;control groups&#xff09;是一个非常强大的linux内核工具&#xff0c;他不仅可以限制被namespace隔离起来的资源&#xff0c; 还可以为资源设置权重、计算使用量、操控进程启停等等。 所以cgroups&#xff08;control groups&#xf…

【IC设计】奇数分频与偶数分频 电路设计(含讲解、RTL代码、Testbench代码)

文章目录 原理分析实现和仿真偶数分频的电路RTL代码偶数分频的电路Testbench代码偶数分频的电路仿真波形占空比为50%的三分频电路RTL代码占空比为50%的三分频电路Testbench代码占空比为50%的三分频电路仿真波形 参考资料 原理分析 分频电路是将给定clk时钟信号频率降低为div_c…

Springboot 整合 Quartz框架做定时任务

在Spring Boot中整合Quartz&#xff0c;可以实现定时任务调度的功能 1、首先&#xff0c;在pom.xml文件中添加Quartz和Spring Boot Starter Quartz的依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-bo…

基于MNIST的手写数字识别

上次我们基于CIFAR-10训练一个图像分类器&#xff0c;梳理了一下训练模型的全过程&#xff0c;并且对卷积神经网络有了一定的理解&#xff0c;我们再在GPU上搭建一个手写的数字识别cnn网络&#xff0c;加深巩固一下 步骤 加载数据集定义神经网络定义损失函数训练网络测试网络 …

AI绘画怎么用涂抹消除处理图片?

AI绘画软件中的涂抹消除功能通常用于处理图片&#xff0c;以去除不需要的部分或进行细节调整。不同的AI绘画软件可能具有不同的界面和功能设置&#xff0c;因此具体的操作步骤可能会有所不同。那么AI绘画一般怎么用涂抹消除处理图片? 该功能主打“一键消除&#xff0c;不留痕迹…

【MCU】栈溢出问题

项目场景&#xff1a; 硬件&#xff1a;STM32F407&#xff0c;操作系统&#xff1a;rt_thread master分支 问题描述 问题栈溢出 id 499 ide 00 rtr 00 len 8 9 Function[rt_completion_wait] shall not be used in ISR (0) assertion failed at function:rt_completion_wait,…

由于磁盘空间不够导致服务无法访问的情况

昨天服务出现了一些“小状况”&#xff0c;这里做下记录&#xff0c;为了以后类似的问题&#xff0c;可以作为参考。 具体情况是&#xff0c;如下&#xff1a; 本来一直访问都好好的服务突然间访问不到了&#xff0c;首先确定了下服务器上的 docker 服务是否正常运行。确认正…

粒子群算法与优化储能策略python实践

粒子群优化算法&#xff08;Particle Swarm Optimization&#xff0c;简称PSO&#xff09;, 是1995年J. Kennedy博士和R. C. Eberhart博士一起提出的&#xff0c;它是源于对鸟群捕食行为的研究。粒子群优化算法的基本核心是利用群体中的个体对信息的共享从而使得整个群体的运动…

【办公类-26-01】20240422 UIBOT网络教研(自动登录并退出多个账号,半自动半人工)

作品展示&#xff1a; 背景需求&#xff1a; 每学期有多次网络教研 因为我有历任搭档的进修编号和登录密码&#xff0c; 所以每次学习时&#xff0c;我会把历任搭档的任务也批量完成。 但是每次登录都要从EXCEL里复制一位老师的“进修编号”“密码”&#xff0c;还要点击多次…

快速回复app是什么样

在电商领域&#xff0c;掌握一些必备的软件工具是提高工作效率、优化运营流程以及提升用户体验的关键。本文将为您介绍做电商必备的几个软件&#xff0c;帮助您更好地开展电商业务。 ​ 快速回复APP&#xff1a;重新定义沟通效率 在快节奏的现代社会中&#xff0c;人们对于沟通…

53.基于微信小程序与SpringBoot的戏曲文化系统设计与实现(项目 + 论文)

项目介绍 本站采用SpringBoot Vue框架&#xff0c;MYSQL数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SpringBoot Vue技术的戏曲文化系统设计与实现管理工作系统化、规范化。 技术选型 后端:…

Aigtek功率放大器的工作特点有哪些方面

功率放大器是电子设备中常见的元器件&#xff0c;用于将输入信号的功率增加到所需的输出功率水平。它在各种应用中发挥着重要作用&#xff0c;如音频放大、射频信号处理、通信系统等。功率放大器具有以下几个工作特点&#xff1a; 放大功能&#xff1a;功率放大器主要的工作特点…

用户请求经过哪些处理(公网)

DNS服务器之间协作&#xff1a; 递归DNS查询&#xff1a;用户的请求首先发送到递归DNS服务器。 查询根DNS服务器&#xff1a;递归DNS服务器查询根DNS服务器&#xff0c;以找到管理.com顶级域的TLD DNS服务器。 查询TLD DNS服务器&#xff1a;根DNS服务器响应带有TLD DNS服务器…

深入Doris实时数仓:导入本地数据

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! < 免责声明 > 避免对文章进行过度解读&#xff0c;因为每个人的知识结构和认知背景不大同&#xff0c;没有一种通用的解决方…

【Java探索之旅】解密构造方法 对象初始化的关键一步

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、对象的构造及初始化1.1 构造方法1.2 构造方法的特性1.3 默认初始化1.4 就地初始化…

新手可以能做视频号小店,其实,视频号远没你想象中难!

大家好&#xff0c;我是电商花花。 最近注意到一个又一个新手小白提供视频号小店成功逆袭&#xff0c;实现了自己的创业梦想。 最近电商行业在飞速发展&#xff0c;越来越多的人开始关注视频号小店这个新兴的市场和平台。 有的新手拼命的往里扎&#xff0c;但是不少新手商家…

数据库之数据库恢复技术思维导图+大纲笔记

大纲笔记&#xff1a; 事务的基本概念 事务 定义 用户定义的一个数据库操作系列&#xff0c;这些操作要么全做&#xff0c;要么全不做&#xff0c;是一个不可分割的基本单位 语句 BEGIN TRANSACTION 开始 COMMIT 提交&#xff0c;提交事务的所有操作 ROLLBACK 回滚&#xff0c…