幂等性原本是数学中的概念,引入到计算机软件设计中,指在多次请求同一资源的情况下,只有第一次请求会对资源产生影响,而后续的重复请求不会对资源造成进一步的影响。这意味着无论操作执行多少次,资源的状态都应该保持一致。 幂等性主要用于处理网络延迟、系统故障或用户重复操作等情况,确保数据的一致性和系统的稳定性,它是服务对外的一种承诺,即使外部调用失败并进行重试,系统的数据状态也不会因此发生变化。
幂等性不仅指操作多次而不产生副作用,如查询数据库,还涵盖了那些初次请求可能改变资源状态,但后续重复请求不再产生进一步影响的场景。 这是服务对外的一种承诺,保证无论多少次执行,只要操作成功,其对系统的影响始终保持一致,从而在面对网络问题和必要的重试时维持数据的稳定性和一致性。
在许多业务场景中,缺乏幂等性设计会带来严重后果,尤其是在涉及金融交易如支付和下单的系统中。
假设一个用户在在线购物平台上购买商品。在支付过程中,用户点击了“支付”按钮提交订单,但由于网络延迟,用户没有立即收到任何反馈。 这种不确定性可能导致用户多次点击“支付”按钮。如果支付操作不是幂等的,每次点击都会触发一个新的支付请求。 那么就可能导致下面几种后果。
主要就是后端来做实现,前端只能尽可能避免但是不能保证,当然,有的公司存在独立的网关或其他基础设施运维团队,那么也可以在这些方面做实现。
客户点击提交订单按钮,但由于网络延迟,客户未看到反馈而再次点击提交。服务器需要处理这种可能的重复提交,确保订单只被创建一次。
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class OrderService {
// 假设这是存储已处理事务的数据库或内存结构
Map<String, OrderResult> processedTransactions = new HashMap<>();
public OrderResult submitOrder(OrderData order, String transactionId) {
// 检查事务ID是否已存在
if (processedTransactions.containsKey(transactionId)) {
// 如果事务ID存在,直接返回之前的处理结果
return processedTransactions.get(transactionId);
} else {
// 处理订单
OrderResult result = processNewOrder(order);
// 存储事务ID与处理结果
processedTransactions.put(transactionId, result);
return result;
}
}
private OrderResult processNewOrder(OrderData order) {
// 这里包含创建订单的逻辑
// ...
return new OrderResult("Success", "Order Created");
}
}
class OrderResult {
String status;
String message;
public OrderResult(String status, String message) {
this.status = status;
this.message = message;
}
}
class OrderData {
// 订单数据结构
// ...
}
OrderService
类使用 submitOrder
方法接受订单数据和一个事务ID。它首先检查是否已处理过相应的事务ID。如果是,则直接返回之前的处理结果,从而防止重复处理订单;如果不是,它会处理订单,并将结果与事务ID关联存储起来。
这种方法确保了即使在多次提交相同事务ID的请求时,系统的行为也是幂等的,避免了重复创建订单等潜在问题。
假设我们有一个在线商店的结账过程,使用令牌机制防止用户因点击结账按钮多次而多次扣款。
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class CheckoutService {
// 存储生成的令牌
Set<String> validTokens = new HashSet<>();
// 请求结账时生成令牌
public String generateToken() {
String token = UUID.randomUUID().toString();
validTokens.add(token);
return token;
}
// 处理结账
public PaymentResult processPayment(String token, PaymentData paymentData) {
if (!validTokens.contains(token)) {
// 如果令牌无效或已使用
return new PaymentResult("Failure", "Invalid or expired token");
} else {
// 执行支付逻辑
performPayment(paymentData);
// 标记令牌为已使用
validTokens.remove(token);
return new PaymentResult("Success", "Payment processed");
}
}
private void performPayment(PaymentData paymentData) {
// 实际的支付处理逻辑
// ...
}
}
class PaymentResult {
String status;
String message;
public PaymentResult(String status, String message) {
this.status = status;
this.message = message;
}
}
class PaymentData {
// 支付数据结构
// ...
}
CheckoutService
类负责结账流程,包括生成令牌、验证令牌的有效性,并处理支付。
这种机制确保即使用户多次点击提交按钮,只要令牌已被使用,重复的请求就不会导致多次扣款,从而实现幂等性。
该方法需要配合前端实现哦!
在数据库层面实现幂等性时,乐观锁和悲观锁是两种常用的锁机制,用于控制数据的并发访问和修改。 这些锁机制能够防止数据冲突和不一致,特别是在高并发的应用场景中。
人如其名,非常乐观,乐观锁默认认为不会出现数据不一致问题。
乐观锁基于这样的假设:数据通常情况下不会发生冲突,因此,在数据库操作时,它先执行操作,然后在提交时检查数据在读取到提交期间是否被其他事务修改过。
假设有一个订单系统,在订单表中每个订单记录包含一个版本号字段,当更新订单信息时,先读取订单数据和其版本号,更新时检查版本号是否发生变化。
UPDATE orders SET status = 'processed', version = version + 1
WHERE order_id = 123 AND version = 1;
如果version
不匹配,意味着另一个事务已经修改了数据,当前更新操作将失败。
乐观锁适用于冲突较少的场景,可以减少锁的开销,提高系统的并发能力。
人如其名,非常悲观,悲观锁默认为数据多半会出现不一致问题。
悲观锁假设数据很可能会被其他事务修改,因此在数据被读取时就锁定它,直到当前事务完成。
我们可以利用数据库提供的锁机制来实现,通常使用行级锁。
在处理订单支付时,为了防止订单被并发修改,可以在查询时锁定订单记录。
SELECT * FROM orders WHERE order_id = 123 FOR UPDATE;
这个查询会锁定ID为123的订单,直到事务完成,其他试图修改此记录的事务必须等待第一个事务完成。
悲观锁适用于高冲突环境,可以直接防止数据冲突,但可能降低并发性能。
这个不必多说,数据库基本都可以设置唯一约束,某一个字段不能重复,否则直接抛出异常。
命令模式是一种行为设计模式,它将一个请求或简单操作封装为一个对象,从而允许用户使用不同的请求、队列或日志请求,并支持可撤销操作。
应用场景:
命令模式可以通过精确控制何时何如何执行操作来保证幂等性,每个命令对象都确保其执行的操作可以安全地重复执行或撤销重做而不影响最终系统状态。
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
interface Command {
void execute();
void undo();
}
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class Light {
public void turnOn() { System.out.println("Light is on"); }
public void turnOff() { System.out.println("Light is off"); }
}
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class TurnOnCommand implements Command {
private Light light;
public TurnOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
public void undo() {
light.turnOff();
}
}
// 使用命令对象
Light light = new Light();
Command switchOn = new TurnOnCommand(light);
switchOn.execute(); // 执行命令
switchOn.undo(); // 撤销命令
使用设计模式解决会带来更多的心智负担。
备忘录模式是一种行为设计模式,它允许在不违反封装原则的情况下捕获并外部化一个对象的内部状态,以便以后可以将该对象恢复到这个状态。
应用场景:
备忘录模式通过保存状态快照来实现幂等性。如果多次执行相同操作,系统可以利用保存的状态快照恢复到初始状态,确保操作的幂等性。
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public Memento saveStateToMemento() {
return new Memento(state);
}
public void getStateFromMemento(Memento memento) {
state = memento.getState();
}
}
/**
* 伪代码
*
* @author JanYork
* @email <747945307@qq.com>
* @date 2024/4/19 下午4:51
*/
class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
// 使用备忘录对象
Originator originator = new Originator();
originator.setState("State #1");
Memento savedState = originator.saveStateToMemento();
originator.setState("State #2");
originator.getStateFromMemento(savedState); // 恢复到State #1
使用设计模式解决会带来更多的心智负担。
时间戳和条件请求是一种确保幂等性和优化资源管理的策略,常用于缓存控制和减少不必要的服务器负载,这种方法通常通过HTTP协议中的条件请求头来实现,如
If-Modified-Since
和ETag
。
工作原理:
If-None-Match
头中)。如果ETag未改变,表明资源未修改,服务器返回304 Not Modified;如果ETag改变,表明资源已更新,服务器则发送新资源。GET /image.png HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
HTTP/1.1 304 Not Modified
Date: Sun, 23 Oct 2016 14:19:41 GMT
这种方法非常适合于处理Web资源,如图片、CSS文件或JavaScript文件,以减少不必要的数据传输。
虽然看上去和令牌机制相似,但是场景不太一样,这个在
CDN
、对象存储
相关领域用的较多。
分布式锁是在多个计算实例间同步访问共享资源的一种机制,用于在分布式系统中实现跨多个节点的操作的原子性和一致性。 一般是通过使用外部协调服务(如Redis、Zookeeper或数据库)来管理锁的状态。 当一个服务实例需要执行对共享资源的操作时,它首先必须从协调服务中获取锁。 如果获取锁成功,该实例执行操作;操作完成后释放锁。 如果锁已被其他实例持有,则当前请求可能需要等待或者直接失败。
// 假设使用Jedis库操作Redis
Jedis jedis = new Jedis("localhost");
String key = "resource_lock";
String token = UUID.randomUUID().toString();
String result = jedis.set(key, token, "NX", "EX", 30); // 尝试获取锁,设置30秒过期
if ("OK".equals(result)) {
try {
// 执行操作
performCriticalTask();
} finally {
// 释放锁
// 使用Lua脚本来确保只有持有锁的实例可以释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(token));
}
} else {
System.out.println("Failed to acquire lock");
}
幂等性被击穿是分布式系统中的一种严重问题,特别是在涉及金融交易或其他关键数据操作的系统中。 幂等性保证一个操作无论执行多少次,结果都应该相同,但在实际情况中,由于系统的复杂性和环境的不可预见性,幂等性可能会被击穿。