查看原文
其他

如何使用 Redis 实现一个排行榜?

编程导航和鱼友们 面试鸭 2024-01-21

大家好呀,今天是编程导航 30 天面试题挑战的第十四天,一起来看看今天有哪些优质面试题吧。

后端

题目一

如何使用 Redis 实现一个排行榜?

官方解析

使用 Redis 可以很方便地实现一个排行榜,以下是一种实现方法:

使用有序集合(Sorted Set)来存储排行榜数据,以用户得分作为分数(score),用户 ID 作为成员(member)。

当用户得分改变时,使用 Redis 的 ZADD 命令将用户的分数更新到有序集合中。

获取排行榜数据时,使用 Redis 的 ZREVRANGE 命令按分数倒序获取有序集合中的成员。

可以使用 Redis 的 ZSCORE 命令获取某个用户的分数,或使用 ZREVRANK 命令获取某个用户的排名。

以下是一个简单的 Node.js 实现:

const redis = require('redis');
const client = redis.createClient();

// 将用户得分更新到排行榜中
function updateScore(userId, score) {
  client.zadd('leaderboard', score, userId, (err, res) => {
    if (err) {
      console.error('Error updating score:', err);
    }
  });
}

// 获取排行榜数据
function getLeaderboard() {
  return new Promise((resolve, reject) => {
    client.zrevrange('leaderboard', 0, -1, 'WITHSCORES', (err, res) => {
      if (err) {
        reject(err);
      } else {
        const leaderboard = [];
        for (let i = 0; i < res.length; i += 2) {
          leaderboard.push({
            userId: res[i],
            score: parseInt(res[i + 1])
          });
        }
        resolve(leaderboard);
      }
    });
  });
}

// 获取某个用户的分数
function getScore(userId) {
  return new Promise((resolve, reject) => {
    client.zscore('leaderboard', userId, (err, res) => {
      if (err) {
        reject(err);
      } else {
        resolve(parseInt(res));
      }
    });
  });
}

// 获取某个用户的排名
function getRank(userId) {
  return new Promise((resolve, reject) => {
    client.zrevrank('leaderboard', userId, (err, res) => {
      if (err) {
        reject(err);
      } else {
        resolve(res === null ? null : res + 1);
      }
    });
  });
}

// 示例代码
updateScore('user1', 100);
updateScore('user2', 200);
updateScore('user3', 300);

getLeaderboard().then((leaderboard) => {
  console.log('Leaderboard:', leaderboard);
});

getScore('user1').then((score) => {
  console.log('User1 score:', score);
});

getRank('user1').then((rank) => {
  console.log('User1 rank:', rank);
});

在实际应用中,还可以根据需要进行排行榜数据的缓存和更新策略等优化。

鱼友的精彩回答

爱吃鱼蛋的回答

Redis 实现排行榜是 Redis 中一个很常见的场景,主要使用的是 ZSet 进行实现,下面是为什么选用 ZSet:

  • 有序性:排行榜肯定需要实现一个排序的功能,在 Redis 中有序的数据结构有 List 和 ZSet;

  • 支持分数操作:ZSet 可以对集合中的元素进行增删改查操作,十分贴合排行榜中用户分数动态变化的场景,而 List 并不能针对分数进行操作,只有其中的 value 进行操作;

  • 支持范围查询:ZSet 可以按照分数进行范围查询,如排行榜中的 Top10 需求就可通过该特性进行实现;

  • 支持去重:由于 ZSet 属于 Set 的特殊数据结构,因此同样拥有 Set 不可重复的特性,对于排行榜中不可出现重复项的需求也十分贴合,而 List 只能手动去重。

因此选择ZSet实现排行榜相对于List实现会更合适和高效。

以学生成绩排行为例,下面是使用Redis命令实现

# 添加示例数据
ZADD scores 90 "张三"
ZADD scores 85 "李四"
ZADD scores 95 "王五"
ZADD scores 92 "赵六"
# 查询排名前3的学生信息
ZRANGE scores 0 2 WITHSCORES
# 查询排名前3的打印
1) "王五"
2) "95"
3) "赵六"
4) "92"
5) "张三"
6) "90"
# 删除学生“李四”的成绩信息
ZREM scores "李四"

下面是SpringBoot整合Redis进行实现

// 添加学生成绩
public void addScore(String name, int score) {
    redisTemplate.opsForZSet().add("scores", name, score);
}

// 查询排名前N的学生成绩
public List<Map.Entry<String, Double>> getTopScores(int n) {
    return redisTemplate.opsForZSet().reverseRangeWithScores("scores", 0, n - 1)
            .stream()
            .map(tuple -> new AbstractMap.SimpleEntry<>(tuple.getValue(), tuple.getScore()))
            .collect(Collectors.toList());
}

// 删除某个学生的成绩
public void removeScore(String name) {
    redisTemplate.opsForZSet().remove("scores", name);
}

题目二

什么是网关,网关有哪些作用?

官方解析

网关(Gateway)是在计算机网络中用于连接两个独立的网络的设备,它能够在两个不同协议的网络之间传递数据。在互联网中,网关是一个可以连接不同协议的网络的设备,比如说可以连接局域网和互联网,它可以把局域网的内部网络地址转换成互联网上的合法地址,从而使得局域网内的主机可以与外界通信。

在计算机系统中,网关可以用于实现负载均衡、安全过滤、协议转换等功能。具体来说,网关可以分为以下几种:

  • 应用网关:用于应用层协议的处理,如 HTTP、SMTP 等。

  • 数据库网关:用于数据库访问的控制和管理。

  • 通信网关:用于不同通信协议之间的数据交换,如 TCP/IP、UDP/IP 等。

  • API 网关:用于管理和转发 API 请求,实现 API 的授权、限流、监控等功能。

总的来说,网关可以为不同网络提供连接和通信的功能,同时也可以提供安全、性能、可靠性等方面的增强功能,是现代计算机系统中不可或缺的一部分。

鱼皮补充:API 网关这里,大家可以提及 Spring Cloud Gateway;应用层网关可以想到 Nginx、HA Proxy 等

鱼友的精彩回答

Starry 的回答

网关(Gateway)是连接两个或多个不同网络的设备,可以实现协议的转换、数据的转发和安全策略的实现等功能。简单来说,网关是设备与路由器之间的桥梁,由它将不同的网络间进行访问的控制,转换,交接等等。

常见的网关有应用网关、协议网关、安全网关等。

网关的作用如下:

  • 实现协议的转换:不同网络之间通常使用不同的协议,通过网关可以实现协议的转换,使得不同网络之间能够相互通信。
  • 提供数据转发功能:网关可以对传输的数据进行过滤、路由、转发等处理,确保数据的可靠传输。
  • 实现安全策略:网关可以对传输的数据进行加密、认证、授权等操作,保证数据的安全性和可靠性。
  • 提供缓存功能:网关可以将一部分数据缓存起来,提高数据的访问速度和响应性能。
  • 支持负载均衡:网关可以将请求分配到不同的服务器上,实现负载均衡,提高系统的可用性和性能。
  • 实现访问控制:网关可以对访问进行控制,防止未授权的访问和攻击,提高系统的安全性。

mos 的回答

在微服务架构里,服务的粒度被进一步细分,各个业务服务可以被独立的设计、开发、测试、部署和管理。这时,各个独立部署单元可以用不同的开发测试团队维护,可以使用不同的编程语言和技术平台进行设计,这就要求必须使用一种语言和平 台无关的服务协议作为各个单元间的通讯方式。

API 网关的定义

网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。

API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。

职能

功能

网关可以分为以下几种:

  • 应用网关:用于应用层协议的处理,如 HTTP、SMTP 等。

  • 数据库网关:用于数据库访问的控制和管理。

  • 通信网关:用于不同通信协议之间的数据交换,如 TCP/IP、UDP/IP 等。API 网关:用于管理和转发 API 请求,实现 API 的授权、限流、监控等功能。

Antony 的回答

网关,即 Gateway,网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性;
  • 存在跨域请求,在一定场景下处理相对复杂;
  • 认证复杂,每个服务都需要独立认证;
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。而划分出多个微服务就代表可能出现多个新的访问地址,如果客户端直接与微服务通信,那么重构将会很难实施;
  • 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难;

那么使用网关的好处就如下:

  • 路由
  • 负载均衡
  • 统一鉴权
  • 跨域
  • 统一业务处理
  • 访问控制
  • 发布控制
  • 流量染色
  • 接口保护
  • 统一日志
  • 统一文档

常见的网关产品有 Tyk,Kong,Zuul 以及 Spring Cloud Gateway

题目三

线程的生命周期是什么,线程有几种状态,什么是上下文切换?

官方解析

线程的生命周期通常包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。其中:

  • 新建状态是指当线程对象创建后,就进入了新建状态。此时它已经有了相应的内存空间和其他资源,但还没有开始运行。
  • 就绪状态是指当线程对象调用 start() 方法后,线程进入了就绪状态。此时它已经具备了运行的条件,只等 CPU 分配资源,让其开始执行。
  • 运行状态是指当线程对象获得 CPU 资源后,就开始执行 run() 方法中的代码,线程处于运行状态。
  • 阻塞状态是指线程在某些特定情况下会被挂起,暂时停止执行。当线程处于阻塞状态时,它并不会占用 CPU 资源。
  • 终止状态是指当线程的 run() 方法执行完毕或者因异常退出时,线程进入了终止状态。此时,该线程不再占用 CPU 资源,也不再执行任何代码。

在线程的生命周期中,线程状态的转换通常是由操作系统调度和控制的。当线程的状态发生变化时,需要进行上下文切换,即保存当前线程的状态和上下文信息,并恢复另一个线程的状态和上下文信息,使其能够继续执行。上下文切换会带来一定的开销,因此需要尽可能减少线程之间的切换次数

鱼友的精彩回答

苏打饼干的回答

从传统操作系统层面,线程有五大基本状态,包括:创建、就绪、运行、阻塞和终止状态

从 Java 并发编程的角度来看,线程有六个状态,包括:新建、就绪、阻塞、等待、超时等待和终止状态

上下文切换:指将当前线程的状态保存下来,并将 CPU 资源切换到另一个线程上运行的过程,通常由操作系统进行管理和控制的。需要注意,上下文切换需要花费一定的时间和系统资源,线程的上下文切换次数要尽量减少,以提高系统的性能。

猫十二懿的回答

线程通常有五种状态:创建,就绪,运行、阻塞和死亡状态

线程通常有五种状态:创建,就绪,运行、阻塞和死亡状态。

  • 新建状态(New):新创建了一个线程对象。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。
  • 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  • 死亡状态(Dead):线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。

其中阻塞的情况又分为三种:

(1)、等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify 或 notifyAll 方法才能被唤醒,wait 是 object 类的方法

(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入“锁池”中。

(3)、其他阻塞:运行的线程执行 sleep 或 join 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep 状态超时、join 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。sleep 是 Thread 类的方法。

前端

题目一

JS 如何顺序执行 10 个异步任务?

官方解析

JS 中可以使用 Promise 和 async/await 来顺序执行异步任务。

使用 Promise 可以通过 then() 方法的链式调用来实现顺序执行异步任务,例如:

function asyncTask1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Async task 1');
      resolve();
    }, 1000);
  });
}

function asyncTask2() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Async task 2');
      resolve();
    }, 2000);
  });
}

// 顺序执行异步任务
asyncTask1().then(() => {
  return asyncTask2();
}).then(() => {
  // 执行完异步任务1和异步任务2后的逻辑
});

使用 async/await 可以将异步任务看作同步代码来执行,例如:

async function runAsyncTasks() {
  await asyncTask1();
  await asyncTask2();
  // 执行完异步任务1和异步任务2后的逻辑
}

runAsyncTasks();

在这里,runAsyncTasks() 函数会先执行异步任务 1,等待异步任务 1 完成后再执行异步任务 2。

鱼友的精彩回答

mos 的回答

Promise 的方式
ction fn1() {
    return new Promise((resolve, reject) => {
        console.log('fn1执行')
        setTimeout(() => {
            console.log('fn1结束')
            resolve('fn1传递过去的参数')
        }, 1000)
    })
}

function fn2(data) {
    return new Promise((resolve, reject) => {
        console.log('fn2执行,接收的参数', data)
        setTimeout(() => {
            resolve('fn2传递过去的参数')
        }, 1000)
    })
}

function fn3(data) {
    return new Promise((resolve, reject) => {
        console.log('fn3执行,接收的参数', data)
        setTimeout(() => {
            resolve('fn3传递过去的参数')
        }, 1000)
    })
}

fn1().then(fn2).then(fn3).then(res => {
    console.log('最后一个', res)
})

执行结果如下:

生成器的方式

生成器就是能返回一个迭代器的函数,它也是一个函数,对比普通的函数,多了一个*,在它的函数体内可以使用yield关键字,函数会在每个yield后暂停,等待,直到这个生成的对象,调用下一个next(),每调用一次next会往下执行一次yieId,然后暂停。

functionmain() {
    const res1 = yield fn1('开始')
    const res2 = yield fn2(res1)
    const res3 = yield fn3(res2)
    console.log(res3, '全部执行完毕')
}

const task = main()
task.next()

function fn1(data) {
    setTimeout(() => {
        console.log('fn1执行', data)
        task.next('fn1执行完毕')
    }, 1000)
}

function fn2(data) {
    setTimeout(() => {
        console.log('fn2执行', data)
        task.next('fn2执行完毕')
    }, 1000)
}

function fn3(data) {
    setTimeout(() => {
        console.log('fn3执行', data)
        task.next('fn3执行完毕')
    }, 1000)
}

console.log('我是最开始同步执行的')

执行结果如下:

async/await

使用 async/await 可以将异步任务看作同步代码来执行,例如:

async function runAsyncTasks() {
  await asyncTask1();
  await asyncTask2();
  // 执行完异步任务1和异步任务2后的逻辑
}

runAsyncTasks();

在这里,runAsyncTasks() 函数会先执行异步任务 1,等待异步任务 1 完成后再执行异步任务 2。

题目二

React 组件间怎么进行通信?

官方解析

React 组件间通信可以通过以下方式实现:

  1. Props 传递:父组件可以通过 Props 将数据传递给子组件,从而实现数据通信。
  2. Context:Context 是 React 提供的一种组件间通信的机制,可以通过 Context 在组件树中传递数据,避免 Props 层层传递的麻烦。
  3. Refs:Refs 允许我们直接操作组件实例或者 DOM 元素,从而实现组件间通信。
  4. 自定义事件:可以通过自定义事件的方式实现组件间的通信。在组件中定义一个事件,当需要在其他组件中触发这个事件时,可以通过回调函数的方式实现。
  5. 全局状态管理:使用全局状态管理工具(如 Redux、Mobx)来管理组件状态,从而实现组件间通信。

需要根据实际场景选择适合的通信方式。

鱼皮补充:这题还是挺重要的,因为组件通信是开发中的一个必备技能,建议大家把以上几种方式都实践一下

题目三

介绍一下 JS 中 setTimeout 的运行机制?

官方解析

在 JavaScript 中,setTimeout() 方法被用于在指定的时间间隔后执行一个指定的代码块。它接受两个参数:第一个参数是需要执行的代码块,第二个参数是代码块的延迟时间(以毫秒为单位)。setTimeout() 方法执行完毕后,代码块将被推入 JavaScript 的执行队列中,等待 JavaScript 引擎在一段时间后执行。

setTimeout() 方法具有异步的特性,因此即使 setTimeout() 方法指定了一个很短的时间,它也不会在调用代码之后立即执行。相反,它会将代码块放在事件队列的末尾,直到事件队列中没有任何待处理的任务,才会执行。因此,当代码块执行时,当前执行的上下文(也称为堆栈)已经被清空。

如果 setTimeout() 方法在代码块执行之前被清除或者代码块执行时间过长,那么代码块将会在 JavaScript 引擎空闲时尽快被执行。因此,setTimeout() 方法不是一个精确的时间控制器,而是一个粗略的时间控制器。如果需要更精确的时间控制器,可以考虑使用 requestAnimationFrame() 或 Web Workers。

鱼友的精彩回答

Kristen 的回答

setTimeout()函数:用来指定某个函数或某段代码在多少毫秒之后执行。它接受两个参数:第一个参数是需要执行的代码块,第二个参数是代码块的延迟时间(以毫秒为单位)。它返回一个整数,表示定时器timer的编号,可以用来取消该定时器。是一个异步函数。

console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
console.log(3);

最后的打印顺序是:1 3 2 无论setTimeout的执行时间是0还是1000,结果都是先输出3后输出2。

任务队列

一个先进先出的队列,它里面存放着各种事件和任务。所有任务可以分成两种,一种是同步任务,另一种是异步任务。

同步任务

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

  1. 输出 如:console.log()
  2. 变量的声明
  3. 同步函数:如果在函数返回的时候,调用者就能够拿到预期的返回值或者看到预期的效果,那么这个函数就是同步的。
异步任务
  1. setTimeout和setInterval
  2. DOM事件
  3. Promise
  4. process.nextTick
  5. fs.readFile
  6. http.get
  7. 异步函数:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
优先关系

异步任务要挂起,先执行同步任务,同步任务执行完毕才会响应异步任务。

JS执行机制

由于 JS 是单线程,所以同一时间只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。为了避免因为某些长时间任务造成的无意义等待,JS 引入了异步的概念,用另一个线程来管理异步任务。

同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程; 等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如 ajax 请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环;

定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。

setTimeout 注册的函数 fn 会交给浏览器的定时器模块来管理,延迟时间到了就将 fn 加入主进程执行队列,如果队列前面还有没有执行完的代码,则又需要花一点时间等待才能执行到 fn,所以实际的延迟时间会比设置的长; 如在 fn 之前正好有一个超级大循环,那延迟时间就不是一丁点了。

setInterval 的实现机制跟 setTimeout 类似,只不过 setInterval 是重复执行的。对于 setInterval(fn, 100) 容易产生一个误区:并不是上一次 fn 执行完了之后再过 100ms 才开始执行下一次 fn。事实上,setInterval 并不管上一次 fn 的执行结果,而是每隔 100ms 就将 fn 放入主线程队列; 而两次 fn 之间具体间隔多久就不一定了,跟 setTimeout 实际延迟时间类似,和 JS 执行情况有关。

鱼皮评论:很棒的回答 👍 OYAMA:从函数到同步异步到执行机制,很全面啊

星球活动

1.欢迎参与 30 天面试题挑战活动 ,搞定高频面试题,斩杀面试官!

2.欢迎已加入星球的同学 免费申请一年编程导航网站会员

3.欢迎学习 鱼皮最新原创项目教程,手把手教你做出项目、写出高分简历!

加入我们

欢迎加入鱼皮的编程导航知识星球,鱼皮会 1 对 1 回答您的问题、直播带你做出项目、为你定制学习计划和求职指导,还能获取海量编程学习资源,和上万名学编程的同学共享知识、交流进步。

💎 加入星球后,您可以:

1)添加鱼皮本人微信,向他 1 对 1 提问,帮您解决问题、告别迷茫!点击了解详情

2)获取海量编程知识和资源,包括:3000+ 鱼皮的编程答疑和求职指导、原创编程学习路线、几十万字的编程学习知识库、几十 T 编程学习资源、500+ 精华帖等!点击了解详情

3)找鱼皮咨询求职建议和优化简历,次数不限!点击了解详情

4)鱼皮直播从 0 到 1 带大家做出项目,已有 50+ 直播、完结 3 套项目、10+ 项目分享,帮您掌握独立开发项目的能力、丰富简历!点击了解详情

外面一套项目课就上千元了,而星球内所有项目都有指导答疑,轻松解决问题

星球提供的所有服务,都是为了帮您更好地学编程、找到理想的工作。诚挚地欢迎您的加入,这可能是最好的学习机会,也是最值得的一笔投资!

长按扫码领优惠券加入,也可以添加微信 yupi1085 咨询星球(备注“想加星球”):

继续滑动看下一个

如何使用 Redis 实现一个排行榜?

编程导航和鱼友们 面试鸭
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存