一.幂等性
什么是幂等性?
在计算机科学和数学领域中,” 幂等性 “虽然源于相同的概念,但其应用和具体含义有所不同
在数学中:幂等性是一个代数性质,描述的是一个操作或函数在多次应用后结果不变的特性
在分布式系统中:幂等性是一个重要的设计原则,它描述的是对某一资源发起一次请求和多次请求,其产生的效果都是相同的,不会因操作次数的增加而改变资源的最终状态
幂等性主要解决什么问题?
幂等性的提出主要解决分布式系统中重复提交问题,比如:像不法分子通过提交重复订单恶意刷单;由于网络波动,系统卡顿,用户多次点击支付按钮,导致重复支付;如果这些问题不能有效的解决,会对系统正常运行带来干扰,也可能带来严重的经济损失
为了解决这一系列问题,目前提出了三种行之有效的解决方案
1.数据库约束:利用数据库的唯一索引,主键等特性,保证数据的唯一性。数据库会对这种重复数据插入进行拦截,避免因为重复操作而导致数据冗余或错误
2.乐观锁:常用于数据库操作中,在更新数据时,会根据预先设置的乐观锁状态来判断是否被其他操作修改过,如果状态符合预期,则进行更新,反之,需重新进行处理,以保证数据一致性和操作的幂等性
3.唯一序列号:在进行操作时,为每个请求分配一个唯一的序列号。操作时判断与该序列号相等则执行
通过一个简单的订单系统来展示如何实现这三种方案
场景描述
假设我们有一个订单系统,用户可以提交订单,我们需要保证以下幂等性:
1.用户不能重复提交相同的订单
2.在更新订单状态时,避免并发冲突
3.通过唯一序列号确保操作的幂等性
1.利用数据库约束实现幂等性
通过在数据库中设置唯一索引或主键约束,确保数据的唯一性,当重复数据尝试插入时,数据库会抛出异常,从而阻止重复操作
数据库设计
CREATE TABLE orders(
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
order_number VARCHAR(50) NOT NULL UNIQUE, --唯一订单号
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Java代码实现
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
//定义数据库连接和配置
public class OrderService{
private static final string DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";//数据库用户名
private static final String PASS = "your_password";//数据库密码
//创建订单的方法
public void createOrder(String orderNumber,int userId,double amount){
//使用参数化查询;(?,?,?)占位符预防SQL注入攻击;插入订单需要提供订单号,用户ID以及订单金额
String sql = "INSERT INTO orders(order_number,user_id,amount) VALUES(?,?,?)";
try(Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)){
//将方法参数绑定到SQL语句的占位符中
pstmt.setString(1,orderNumber);
pstmt.setString(1,orderNumber);
pstmt.setDouble(3,amount);
//调用executeUpdate()方法执行插入操作
pstmt.executeUpdate();
System.out.println("订单创建成功!");
}catch(SQLException e){
if(e.getErrorCode() == 1062){ //禁止插入重复数据
System.out.println("订单已存在,无法重新创建!");
}else{
System。out.println("数据库错误:" + e.getMessage());
}
}
}
//主方法创建实例,调用createOrder方法两次,第一次正常插入订单,第二次尝试插入相同订单号
public static void main(String[] args){
OrderService service = new OrderService();
service.createOrder("ORDER123",1,100.0);
service.createOrder("ORDER123",1,100.0); //重复提交
}
}
}
在数据表设计中
order_number字段被设置为唯一,如果没有设置唯一性约束,这段代码就无法正确实现幂等性
结果:
- 第一次提交成功
- 第二次提交时,数据库会抛出唯一性约束错误,阻止重复插入
2.利用乐观锁实现幂等性
在数据库中增加一个版本号字段(如version),每次更新数据时,检查版本号是否与预期一致,如果一致,则更新数据并递增版本号
数据库设计(为了实现乐观锁,数据表中需要有一个版本号字段 version)
ALTER TABLE orders ADD COLUMN version INT DEFAULT 1;
Java代码实现
public void updateOrderStatus(String orderNumber,String newStatus){
//将订单的状态更新为newStatus,并递增版本号,只有当订单号匹配且版本号匹配,才会执行该操作
String sql = "UPDATE orders SET status = ?,version = version + 1 WHERE order_number = ? AND version = ?";
String querySql = "SELECT version FROM orders WHERE order_number = ?";
try(Connection conn = DriverManager.getConnection(DB_URL,USER,PASS)){}
//查询当前订单的版本号
PreparedStatement queryStmt = conn.prepareStatement(querySql)
queryStmt.setString(1,orderNumber);
ResultSet rs = queryStmt.executeQuery();
if(rs.next()){
int currentVersion = rs.getInt("version");
//使用PreparedStatement执行更新操作
try (PreparedStatement updateStmt = conn.prepareStatement(sql)) {
updateStmt.setString(1, newStatus); //新的订单状态
updateStmt.setString(2, orderNumber); //订单号
updateStmt.setInt(3, currentVersion); //当前版本号
//调用executeUpdate()方法执行更新操作
int rowsAffected = updateStmt.executeUpdate();
if (rowsAffected == 0) {
System.out.println("订单状态更新失败,数据已被其他操作修改!");
} else {
System.out.println("订单状态更新成功!");
}
}
} else {
System.out.println("订单不存在!");
}
}
} catch (SQLException e) {
System.out.println("数据库错误:" + e.getMessage());
}
}
//创建OrderService实例,调用updateOrderStatus方法两次,模拟并发更新
public static void main(String[] args) {
OrderService service = new OrderService();
service.updateOrderStatus("ORDER123", "COMPLETED");
service.updateOrderStatus("ORDER123", "CANCELLED"); // 并发更新
}
...
结果:
第一次调用:
- 查询到当前版本号(假设为1)
- 更新成功,版本号递增为2
- 打印:“订单状态更新成功!”
第二次调用:
- 查询到当前版本号(现在是2)
- 尝试更新时,版本号不匹配(预期版本号为1,实际版本号为2)
- 打印:“订单状态更新失败,数据已被其他操作修改!”
3.利用唯一序列号实现幂等性
为每个请求分配一个唯一的序列号(如UUID),并利用数据库的NOT EXISTS条件,在执行操作时,检查该系列号是否已存在,如果存在,则跳过操作
数据库设计(为了实现幂等性,需要设计两个表)
1.订单表:(存储订单的基本信息)
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
order_number VARCHAR(50) NOT NULL UNIQUE,--order_number是订单的唯一标识
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2.操作记录表(用于记录对订单的操作)
CREATE TABLE order_operations (
operation_id VARCHAR(50) PRIMARY KEY,--唯一标识,使用UUID生成
order_id INT NOT NULL,
operation_type VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Java代码实现
import java.util.UUID;
public void processOrderOperation(String orderNumber, String operationType) {
String operationId = UUID.randomUUID().toString(); // 生成唯一序列号
//将操作记录插入到order_operations表中
String sql = "INSERT INTO order_operations (operation_id, order_id, operation_type) "
+ "SELECT ?, (SELECT order_id FROM orders WHERE order_number = ?), ? "
+ "WHERE NOT EXISTS (SELECT 1 FROM order_operations WHERE operation_id = ?)"; //使用WHERE NOT EXISTS子句,检查是否已经存在相同的operation_id
//建立数据库连接并执行SQL语句
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, operationId);
pstmt.setString(2, orderNumber);
pstmt.setString(3, operationType);
pstmt.setString(4, operationId);
int rowsAffected = pstmt.executeUpdate();
if (rowsAffected > 0) {
System.out.println("操作成功执行!");
} else {
System.out.println("操作已被处理,跳过重复执行!");
}
} catch (SQLException e) {
System.out.println("数据库错误:" + e.getMessage());
}
}
//创建OrderService实例,调用processOrderOperation方法两次,模拟重复操作
public static void main(String[] args) {
OrderService service = new OrderService();
service.processOrderOperation("ORDER123", "PAYMENT");
service.processOrderOperation("ORDER123", "PAYMENT"); // 重复操作
}
结果:
第一次调用:
- 生成一个新的operation_id
- 插入操作记录成功,打印“操作成功执行!”
第二次调用
- 生成一个新的operation_id
- 由于幂等性检查(WHERE NOT EXISTS),不会重复插入操作记录,打印“操作已被处理,跳过重复执行!
幂等性保证:即使多次调用该方法,最终结果与调用一次相同,满足幂等性要求