美团VS饿了么,到底谁更胜一筹?
最近啊,收到一个粉丝的投稿,我发现他在美团和饿了么都去面试过。
这俩企业大家应该都经常用吧,咱点外卖的时候,我有时候就琢磨,到底他俩谁更厉害点。
今天咱们就瞅瞅,在面试这块儿谁更难一些。
(目前都只有一面的情况,要是想要后续的,私聊我发给你哈)
美团
一面
- 自我介绍
- 项目做完了吗?背景是什么?项目初期的背景调研是怎么做的?现在这个系统做到哪一步了?
- 用户下单用户派送的优劣了解过吗?怎么管理?
- 项目里面遇到的最大的难题是什么?为什么?
- 超卖,重复下单怎么解决的?
超卖问题:
- 实时库存更新:确保库存数据实时更新,避免销售超出实际库存量。
- 库存预留:对于已经下单但尚未发货的商品,进行库存预留,避免其他订单超卖。
- 库存预警系统:设置库存预警值,当库存低于某个阈值时,自动暂停销售。
- 多渠道同步:如果商品在多个平台销售,确保所有渠道的库存信息同步更新。
重复下单问题:
- 订单确认机制:在用户提交订单后,通过邮件或短信确认订单详情,避免用户误操作重复下单。
- 订单锁定时间:设置一个订单锁定时间,在此时间内用户不能重复下单。
- 用户行为分析:分析用户下单行为,识别异常模式,如短时间内多次下单同一商品,可以进行人工审核或自动拦截。
- 技术手段:使用技术手段,如设置cookie或session,来识别和防止同一用户在短时间内重复下单。
- 为什么使用乐观锁?你了解乐观锁的使用场景和实现逻辑吗?
乐观锁的使用场景:
乐观锁适用于并发冲突较少、读操作远多于写操作的场景。例如:
- 电商平台的商品库存扣减,当并发量不是特别大且库存更新冲突不频繁时可以使用。
- 论坛或社交平台的帖子点赞、评论数量的更新。
- 用户信息的更新,如修改个人资料等。
实现逻辑:
乐观锁通常基于版本号或时间戳来实现。以下是一个基于版本号的乐观锁实现示例:
假设有一张商品表 products
,包含字段 id
(商品 ID)、stock
(库存数量)和 version
(版本号)。
CREATE TABLE products (
id INT PRIMARY KEY,
stock INT,
version INT
);
在更新库存时,先获取当前的版本号,然后在更新操作中判断版本号是否未发生变化。如果版本号未变,说明没有其他并发操作修改了数据,更新成功;否则,更新失败,需要重新获取数据再次尝试更新。
package main
import (
"errors"
"fmt"
)
type Product struct {
ID int
Stock int
Version int
}
// updateStock 更新商品库存
func updateStock(productID int, quantity int) error {
// 先查询当前商品的库存和版本号
product, err := findProductById(productID)
if err!= nil {
return err
}
currentStock := product.Stock
currentVersion := product.Version
// 计算新的库存
newStock := currentStock - quantity
// 尝试更新库存并检查版本号是否未变
updated, err := updateStockAndVersion(newStock, currentVersion+1, productID, currentVersion)
if err!= nil {
return err
}
if!updated {
return errors.New("Concurrent update occurred. Please retry.")
}
fmt.Println("库存更新成功")
return nil
}
// findProductById 根据 ID 查找商品
func findProductById(productID int) (Product, error) {
// 这里模拟查找商品,实际可能从数据库或其他数据源获取
for _, p := range products {
if p.ID == productID {
return p, nil
}
}
return Product{}, errors.New("Product not found")
}
// updateStockAndVersion 执行更新库存和版本号的操作
func updateStockAndVersion(newStock int, newVersion int, productID int, oldVersion int) (bool, error) {
// 这里模拟数据库更新操作,实际可能执行 SQL 语句
for i, p := range products {
if p.ID == productID && p.Version == oldVersion {
products[i].Stock = newStock
products[i].Version = newVersion
return true, nil
}
}
return false, nil
}
var products = []Product{
{ID: 1, Stock: 10, Version: 1},
}
func main() {
err := updateStock(1, 5)
if err!= nil {
fmt.Println(err)
}
}
在上述示例中,updateStock
方法是在数据库中执行的更新操作,对应的 SQL 语句类似于:
UPDATE products
SET stock =?, version =?
WHERE id =? AND version =?;
通过这种方式,实现了乐观锁的机制,保证了在并发环境下数据更新的准确性和一致性。
- 乐观锁怎么实现的你了解吗?
- 了解悲观锁吗?
悲观锁是一种数据库并发控制的机制,它基于一种悲观的假设,即认为在数据处理过程中,并发操作很可能会导致冲突,因此在获取数据时就对数据进行加锁,以防止其他事务对数据进行修改,直到当前事务完成并释放锁。
例如,在关系型数据库中,常见的悲观锁实现方式有 SELECT... FOR UPDATE
语句。当一个事务执行这样的查询时,它会锁定被查询的数据行,其他事务在该锁被释放之前无法修改这些行。
悲观锁的优点在于能够有效地避免并发冲突,保证数据的一致性和准确性。但它也有一些缺点,比如可能导致较大的并发开销,因为在获取锁和释放锁的过程中会消耗系统资源,并且可能会造成死锁等问题。
举个例子,假设有两个事务同时尝试更新同一个用户的账户余额。事务 A 先获取了悲观锁,对余额进行修改。在事务 A 未完成之前,事务 B 无法获取锁,只能等待事务 A 完成并释放锁后才能进行操作。这样就避免了事务 B 在事务 A 未完成时对余额进行不一致的修改。
- 最开始有没有考虑乐观锁的适用场景和悲观锁的适用场景?
- 乐观锁会不会导致频繁的冲突啊?这种情况下和悲观锁谁的性能更好一些呢?
- 乐观锁:在冲突较少的情况下,乐观锁的性能更好,因为它减少了锁的开销。但在高并发或写操作较多的场景下,可能会频繁发生冲突,导致更新失败,从而影响性能。
- 悲观锁:在写操作较多或数据一致性要求较高的场景下,悲观锁更可靠。但悲观锁会降低系统的并发性能,因为它需要在事务开始时就锁定数据。
- 一开始为什么没考虑到呢?是调研不充分吗?
- Redis怎么解决超卖问题的?为什么能解决这个问题?
主要通过以下几种方式:
-
使用 Redis 作为库存计数器:
- 将每个商品的库存数量存储在 Redis 中,每次商品被购买时,从 Redis 中减去相应的库存数量。
- 通过 Redis 的原子操作(如
DECRBY
命令),可以确保库存的增减是原子性的,从而避免并发问题。
-
设置库存预警:
- 在 Redis 中设置一个库存预警值。当库存数量低于这个值时,触发相应的操作,如暂停销售或提醒管理员。
- 这可以通过 Redis 的发布/订阅功能实现,当库存减少到预警值时,发布一个消息,订阅者(如前端页面或后台服务)接收到消息后进行相应的处理。
-
使用 Redis 锁:
- 使用 Redis 的 SETNX 命令实现分布式锁。当一个事务需要操作库存时,先尝试获取锁。如果获取成功,则进行库存操作;如果失败,则等待或重试。
- 这种方式可以避免多个事务同时修改库存,从而防止超卖。
-
使用 Lua 脚本:
- 将库存检查和减少的操作封装在一个 Lua 脚本中,利用 Redis 的
EVAL
命令执行。Lua 脚本在 Redis 服务器上执行,保证了操作的原子性。 - 这种方式可以减少网络开销,提高性能。
- 将库存检查和减少的操作封装在一个 Lua 脚本中,利用 Redis 的
-
使用事务:
- 虽然 Redis 的单条命令是原子的,但多个命令的组合操作可以通过事务(如
MULTI
和EXEC
命令)来保证原子性。 - 使用事务可以确保一系列操作要么全部成功,要么全部失败,从而避免超卖。
- 虽然 Redis 的单条命令是原子的,但多个命令的组合操作可以通过事务(如
-
监控和日志:
- 利用 Redis 的监控功能,记录库存操作的日志。这有助于事后分析和排查问题。
- 可以结合 Redis 的慢查询日志,监控库存操作的性能,及时发现并处理潜在的问题。
为什么 Redis 能解决超卖问题?
- 高性能:Redis 操作速度快,响应时间短,适合高并发场景。
- 原子性:Redis 提供原子操作,如
INCR
、DECR
、SETNX
等,可以保证库存的增减操作是原子性的。 - 分布式锁:Redis 支持分布式锁,可以避免多个进程或线程同时修改库存。
- 可扩展性:Redis 可以水平扩展,支持大规模的数据存储和高并发访问。
- 易于集成:Redis 与许多编程语言和框架都有良好的集成,易于在现有系统中实现库存管理。
通过这些机制,Redis 能够有效地帮助解决超卖问题,确保库存的准确性和一致性。
- 关于Redis的递减特性你了解哪些?
Redis 的递减操作指的是对存储在 Redis 中的某个键对应的值进行减法操作。Redis 提供了decr
和decrby
命令来实现递减功能。
decr
命令用于将键的值减 1。如果键不存在,那么键的值会先被初始化为 0,然后再执行递减操作。其基本语法如下:
DECR key_name
例如,对一个存在的键执行decr
操作:
SET failure_times 10
DECR failure_times
上述操作会将failure_times
键对应的值减 1,结果为 9。
decrby
命令用于将键的值减去给定的整数值。语法如下:
DECRBY key_name decrement
例如,要将键的值减去 5,可以使用:
DECRBY some_key 5
递减操作常用于计数场景,比如统计网站的访问量、文章的点赞数的减少等。例如在一个在线论坛中,每篇文章的点赞数可以通过 Redis 的递减操作来实现取消点赞时点赞数的更新。
- 关于Redis的指令还用到过其他哪些呢?
- setnx的原理你知道吗?
SETNX
是SET IF NOT EXISTS
的缩写,意思是“如果不存在,则设置”。它的原理是在指定的键不存在时,为键设置指定的值。
在 Redis 中,当执行SETNX key value
命令时,如果键key
不存在,那么 Redis 会将值value
设置到键key
中,并返回1
,表示设置成功;如果键key
已经存在,那么不会进行任何操作,并返回0
,表示设置失败。
这种特性使得SETNX
命令常用于实现分布式锁或在特定条件下进行数据设置的场景。例如,在多个客户端或线程竞争资源的情况下,可以使用SETNX
来确保只有一个客户端能够成功设置键值,从而获得某种资源或执行特定操作的权限。
为了防止获得锁的客户端出现异常而导致锁无法释放,造成死锁问题,通常还会结合设置键的过期时间来使用SETNX
。例如,可以使用SETNX key value PX milliseconds
命令,其中PX milliseconds
表示设置键的过期时间为指定的毫秒数。这样,即使客户端未能正常释放锁,当过期时间到达后,Redis 也会自动删除该键,从而释放锁资源。
- 有个场景你了解吗:锁获取后程序退出了,这样锁永远不会释放,导致死锁
- 看你简历里说比较了解集合,对集合的了解简单说一下
集合(Set)是一种常见的数据结构。
集合的主要特点包括:
- 元素的唯一性:集合中不会存在重复的元素。
- 无序性:集合中的元素没有特定的顺序。
集合常用于以下场景:
- 去重操作:例如去除一个数组或列表中的重复元素。
- 快速成员判断:可以高效地判断一个元素是否在集合中。
- 了解HashMap吗?长度为什么是2的幂?
- HashMap的扩容机制
- 为什么加载因子是0.75
- HashMap什么时候把链表转换为红黑树?什么时候红黑树变为链表?
- concurrentHashMap了解吗?实际项目中用过吗?
- 聊一聊Java的垃圾收集机制吧
- 怎样判断内存是否需要回收?(对象死亡的判断方法?)
- 可达性分析是如何对对象进行标记的?三色标记法了解吗?
- 垃圾回收的过程(针对某一个具体的垃圾收集器CMS或G1)
- 清除的过程呢?
- 标记清除算法的优点和缺点?
- CMS标记阶段比较长,如果产生了过多的浮动垃圾,有可能会出现回收赶不上分配的情况,从而导致GC失败,这种要怎么解决?
- JDK后续几个版本的垃圾收集器?更新了什么你了解吗?
- CMS和G1的比对了解吗?为什么废除了CMS?
- G1怎么做到指定具体的垃圾清除时间的?
- 线程的生命周期你了解吗?
- 每个状态之间怎么流转的你了解吗?
- 线程池了解哪些?
- 怎么确定线程池参数知道吗?
- 手撕算法,相似题目力扣940不同的子序列Ⅱ
饿了么
一面
- 自我介绍
- Java面向对象的三个特性,什么是多态
- 双亲委派机制
- HashMap在JDK1.7以前有线程不安全,怎么个情况
- 保证线程安全用什么Map
- ConcurrentHashMap是怎样的
- 线程池用过吗?核心参数以及有啥作用
- 使用线程池有什么好处
- 悲观锁和乐观锁是怎么样子的
- Java中怎么实现悲观锁和乐观锁
- HTTP和HTTPS的区别,加密过程是怎样的
-
HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
-
HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
-
两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
-
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
加密过程
- 客户端发起请求:客户端向服务器发送连接请求,请求中包含支持的加密算法等信息。
- 服务器响应:服务器收到请求后,选择一种双方都支持的加密算法,并返回数字证书给客户端。
- 客户端验证证书:客户端验证服务器证书的合法性,包括证书的颁发机构、有效期、域名等。如果证书验证通过,客户端会生成一个随机的对称密钥。
- 密钥交换:客户端使用服务器的公钥对对称密钥进行加密,并发送给服务器。
- 服务器解密:服务器使用自己的私钥解密得到对称密钥。
- 数据传输:此后,双方使用对称密钥对传输的数据进行加密和解密,完成安全的数据通信。
- TCP的粘包是怎样的现象
TCP 粘包现象:
TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议。在 TCP 传输数据时,可能会出现“粘包”的现象。
“粘包”指的是接收方收到的数据包并不是按照发送方发送的顺序和边界来接收的。例如,发送方先后发送了两个数据包 Packet1
和 Packet2
,但接收方可能一次性接收到了这两个数据包连在一起的数据,而无法明确区分它们之间的边界。
造成 TCP 粘包的主要原因有以下几点:
- 应用程序写入的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入的数据一次性发送出去,导致粘包。
- 接收方的应用程序读取数据的速度较慢,而发送方发送数据的速度较快,这会导致接收方缓冲区中积累多个数据包,当接收方读取时,可能会一次性读出多个数据包,形成粘包。
为了解决 TCP 粘包问题,可以在应用层采取一些措施,比如:
- 明确数据包的边界:可以在数据包中添加特定的分隔符,接收方根据分隔符来区分不同的数据包。
- 固定数据包的长度:发送方和接收方约定每个数据包的固定长度,接收方按照固定长度来读取数据包。
- 在数据包头部添加长度字段:发送方在数据包头部指明数据包的长度,接收方根据长度来读取完整的数据包。
例如,一个即时通讯应用中,发送方发送多条消息,如果不处理粘包问题,接收方可能会将多条消息混在一起,导致显示混乱。通过上述解决方法,可以确保每条消息都能被正确地接收和处理。
- Cookie和session的区别
Cookie 和 Session 的区别:
-
存储位置:
- Cookie 数据存储在客户端(浏览器)。
- Session 数据存储在服务器端。
-
安全性:
- Cookie 存储在客户端,容易被篡改或窃取,安全性相对较低。
- Session 数据存储在服务器,安全性较高。
-
存储容量:
- Cookie 有大小限制,通常为 4KB 左右。
- Session 存储容量通常没有严格的限制,取决于服务器的配置。
-
有效期:
- Cookie 可以设置较长的有效期,并且在有效期内会自动发送给服务器。
- Session 通常依赖于会话,在一段时间内没有活动后会过期,或者可以通过程序手动设置有效期。
-
性能影响:
- 大量使用 Cookie 会增加客户端和服务器之间的通信数据量。
- Session 存储在服务器端,如果存储的 Session 数据过多,可能会消耗服务器的内存资源。
-
跨域支持:
- Cookie 可以在不同的子域之间共享(通过设置 domain 属性)。
- Session 一般不能在不同的应用程序或不同的服务器之间共享。
例如,在一个购物网站中,用户将商品添加到购物车,购物车的信息可以存储在 Cookie 中,以便用户在下次访问时仍然能看到之前添加的商品。但如果涉及到更敏感的用户身份验证信息,通常会使用 Session 来存储。
再比如,一个多服务器的应用,如果使用 Session 来保存用户状态,可能需要配置共享的 Session 存储(如 Redis 存储),以确保用户在不同服务器上的请求能够获取到正确的 Session 数据;而如果使用 Cookie ,则相对更容易处理,但要注意保护 Cookie 中的敏感信息。
- 用户登录之后怎么找到对应的Session的呢
通常会通过以下几种方式找到对应的 Session :
- 基于 Cookie :服务器在用户登录成功后,会生成一个唯一的 Session ID ,并将其存储在 Cookie 中发送给客户端。后续客户端每次向服务器发送请求时,都会自动携带这个 Cookie 。服务器通过解析 Cookie 中的 Session ID ,就能够找到对应的 Session 数据。
例如,服务器设置的 Cookie 可能类似于:JSESSIONID=123456789
,其中 123456789
就是 Session ID 。
-
URL 重写 :如果客户端浏览器不支持 Cookie ,或者出于某些安全原因不能使用 Cookie ,可以通过在 URL 中添加 Session ID 来实现。例如:
http://example.com/page?SESSIONID=123456789
。 -
隐藏表单字段 :在登录成功后的页面中,可以包含一个隐藏的表单字段,其中存储了 Session ID 。当表单提交时,服务器可以获取到这个 Session ID 来找到对应的 Session 。
- 两个项目哪个有挑战一点,挑一个讲一讲
- 平时做项目或科研会遇到什么困难,遇到困难一般自己解决吗
- 最近有了解一些新的技术或者看一些技术书籍之类的吗
- 算法题:反转链表
- 其他的offer
- 之后有往上海发展的打算吗
- 反问
欢迎关注 ❤
我的文章都首发在同名公众号:王中阳
需要简历优化或者就业辅导,可以直接加我微信:wangzhongyang1993,备注:博客园