如何用 Redis 实现一个排行榜?
The following article is from 面试鸭 Author 编程导航和鱼友们
大家好呀,今天继续给大家分享一些优质面试题,希望对正在找工作的小伙伴有所帮助。
后端
题目一
如何使用 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,然后暂停。
function* main() {
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 组件间通信可以通过以下方式实现:
Props 传递:父组件可以通过 Props 将数据传递给子组件,从而实现数据通信。 Context:Context 是 React 提供的一种组件间通信的机制,可以通过 Context 在组件树中传递数据,避免 Props 层层传递的麻烦。 Refs:Refs 允许我们直接操作组件实例或者 DOM 元素,从而实现组件间通信。 自定义事件:可以通过自定义事件的方式实现组件间的通信。在组件中定义一个事件,当需要在其他组件中触发这个事件时,可以通过回调函数的方式实现。 全局状态管理:使用全局状态管理工具(如 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。
任务队列
一个先进先出的队列,它里面存放着各种事件和任务。所有任务可以分成两种,一种是同步任务,另一种是异步任务。
同步任务
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
输出 如:console.log() 变量的声明 同步函数:如果在函数返回的时候,调用者就能够拿到预期的返回值或者看到预期的效果,那么这个函数就是同步的。
异步任务
setTimeout和setInterval DOM事件 Promise process.nextTick fs.readFile http.get 异步函数:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
优先关系
异步任务要挂起,先执行同步任务,同步任务执行完毕才会响应异步任务。
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 咨询星球(备注“想加星球”):