最近啊,收到一个粉丝的投稿,我发现他在美团和饿了么都去面试过。
这俩企业大家应该都经常用吧,咱点外卖的时候,我有时候就琢磨,到底他俩谁更厉害点。
今天咱们就瞅瞅,在面试这块儿谁更难一些。
(目前都只有一面的情况,要是想要后续的,私聊我发给你哈)
超卖问题:
重复下单问题:
乐观锁的使用场景:
乐观锁适用于并发冲突较少、读操作远多于写操作的场景。例如:
实现逻辑:
乐观锁通常基于版本号或时间戳来实现。以下是一个基于版本号的乐观锁实现示例:
假设有一张商品表 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 作为库存计数器:
DECRBY
命令),可以确保库存的增减是原子性的,从而避免并发问题。设置库存预警:
使用 Redis 锁:
使用 Lua 脚本:
EVAL
命令执行。Lua 脚本在 Redis 服务器上执行,保证了操作的原子性。使用事务:
MULTI
和 EXEC
命令)来保证原子性。监控和日志:
为什么 Redis 能解决超卖问题?
INCR
、DECR
、SETNX
等,可以保证库存的增减操作是原子性的。通过这些机制,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 的递减操作来实现取消点赞时点赞数的更新。
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)是一种常见的数据结构。
集合的主要特点包括:
集合常用于以下场景:
HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
加密过程
TCP 粘包现象:
TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议。在 TCP 传输数据时,可能会出现“粘包”的现象。
“粘包”指的是接收方收到的数据包并不是按照发送方发送的顺序和边界来接收的。例如,发送方先后发送了两个数据包 Packet1
和 Packet2
,但接收方可能一次性接收到了这两个数据包连在一起的数据,而无法明确区分它们之间的边界。
造成 TCP 粘包的主要原因有以下几点:
为了解决 TCP 粘包问题,可以在应用层采取一些措施,比如:
例如,一个即时通讯应用中,发送方发送多条消息,如果不处理粘包问题,接收方可能会将多条消息混在一起,导致显示混乱。通过上述解决方法,可以确保每条消息都能被正确地接收和处理。
Cookie 和 Session 的区别:
存储位置:
安全性:
存储容量:
有效期:
性能影响:
跨域支持:
例如,在一个购物网站中,用户将商品添加到购物车,购物车的信息可以存储在 Cookie 中,以便用户在下次访问时仍然能看到之前添加的商品。但如果涉及到更敏感的用户身份验证信息,通常会使用 Session 来存储。
再比如,一个多服务器的应用,如果使用 Session 来保存用户状态,可能需要配置共享的 Session 存储(如 Redis 存储),以确保用户在不同服务器上的请求能够获取到正确的 Session 数据;而如果使用 Cookie ,则相对更容易处理,但要注意保护 Cookie 中的敏感信息。
通常会通过以下几种方式找到对应的 Session :
例如,服务器设置的 Cookie 可能类似于:JSESSIONID=123456789
,其中 123456789
就是 Session ID 。
URL 重写 :如果客户端浏览器不支持 Cookie ,或者出于某些安全原因不能使用 Cookie ,可以通过在 URL 中添加 Session ID 来实现。例如:http://example.com/page?SESSIONID=123456789
。
隐藏表单字段 :在登录成功后的页面中,可以包含一个隐藏的表单字段,其中存储了 Session ID 。当表单提交时,服务器可以获取到这个 Session ID 来找到对应的 Session 。
我的文章都首发在同名公众号:王中阳
需要简历优化或者就业辅导,可以直接加我微信:wangzhongyang1993,备注:博客园
理解并合理运用Spring Boot配置加载的优先级,对于保障应用的安全性、可维护性以及降低部署复杂度至关重要。特别是在大规模微服务架构中,合理的配置管理和迁移对于整体系统的稳定性有着不可忽视的作用。