网站首页 > java教程 正文
场景描述
我正在开发一个在线支付系统,用户在完成支付操作后,系统需要更新订单状态为 “已支付”,并扣除相应的库存。在高并发场景下,由于网络波动等原因,部分用户的支付请求可能会被重复发送。在一次促销活动中,短时间内有大量用户同时进行支付,系统每秒可能会接收到数以千计的支付请求。如果没有有效的防重机制,就可能出现同一笔订单被多次更新状态和重复扣除库存的情况,这将给商家带来巨大的经济损失。
问题分析
出现重复操作的根本原因在于高并发环境下,多个线程同时对共享资源(如订单状态、库存数据)进行操作。由于线程执行的不确定性,当多个线程同时检测到订单未支付且库存充足时,就会同时执行更新订单状态和扣除库存的操作,从而导致”超卖现象“。从 Java 代码层面来看,假设场景:
public class PaymentService {
private Order order;
private Inventory inventory;
public void processPayment() {
if (order.getStatus() == OrderStatus.UNPAID && inventory.hasStock()) {
order.setStatus(OrderStatus.PAID);
inventory.deductStock();
}
}
}
在高并发情况下,多个线程同时进入processPayment方法,并且都通过了if条件判断,进而导致重复操作。
数据库层面的防重
唯一索引
在数据库表中针对关键业务字段(如订单号、交易流水号等)创建唯一索引。当插入重复数据时,数据库会抛出唯一约束冲突异常,应用程序可以捕获该异常并进行相应处理,告知用户操作重复。例如,在订单表中对订单号字段创建唯一索引:
CREATE UNIQUE INDEX idx_order_number ON orders (order_number);
在 Java 代码中,当插入订单数据时:
try {
// 执行插入订单的SQL语句
jdbcTemplate.update("INSERT INTO orders (order_number, amount, status) VALUES (?,?,?)", orderNumber, amount, status);
} catch (DataIntegrityViolationException e) {
// 捕获唯一约束冲突异常,提示用户订单已存在
System.out.println("订单已存在,请勿重复提交");
}
乐观锁
利用数据库的乐观锁机制,在更新数据时,通过版本号或时间戳来确保数据在更新前没有被其他线程修改。例如,在订单表中添加一个version字段:
CREATE TABLE orders (
id INT PRIMARY KEY,
order_number VARCHAR(50),
amount DECIMAL(10, 2),
status VARCHAR(20),
version INT
);
在 Java 代码中,更新订单状态时:
int updatedRows = jdbcTemplate.update("UPDATE orders SET status =?, version = version + 1 WHERE order_number =? AND version =?", OrderStatus.PAID, orderNumber, currentVersion);
if (updatedRows == 0) {
// 说明数据已被其他线程修改,提示用户操作失败或重试
System.out.println("操作失败,请重试");
}
应用层面的防重
基于内存的缓存
使用如 Redis 等内存缓存来存储已经处理过的请求标识(如订单号)。在处理请求前,先查询缓存中是否存在该标识。如果存在,则说明该请求已被处理过,直接返回结果;否则,将请求标识存入缓存,并处理请求。以下是使用 Jedis 操作 Redis 实现防重的示例代码:
public class PaymentService {
private Jedis jedis;
public PaymentService() {
jedis = new Jedis("localhost", 6379);
}
public void processPayment(String orderNumber) {
String key = "processed_order:" + orderNumber;
if (jedis.exists(key)) {
System.out.println("订单已处理,请勿重复提交");
return;
}
// 处理支付逻辑
//...
// 支付成功后,将订单号存入缓存,设置过期时间为防止缓存数据过多
jedis.setex(key, 3600, "processed");
}
}
分布式锁
在分布式系统中,可以使用分布式锁来确保同一时间只有一个线程能够处理某个请求。例如,使用 Redis 的 SETNX(SET if Not eXists)命令来实现分布式锁:
public class DistributedLock {
private Jedis jedis;
private String lockKey;
private String requestId;
private int expireTime;
public DistributedLock(Jedis jedis, String lockKey, int expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.requestId = UUID.randomUUID().toString();
this.expireTime = expireTime;
}
public boolean tryLock() {
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
return "OK".equals(result);
}
public void unlock() {
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(lockKey), Collections.singletonList(requestId));
}
}
在支付服务中使用分布式锁:
public class PaymentService {
private Jedis jedis;
public PaymentService() {
jedis = new Jedis("localhost", 6379);
}
public void processPayment() {
DistributedLock lock = new DistributedLock(jedis, "payment_lock", 10);
if (lock.tryLock()) {
try {
// 处理支付逻辑
//...
} finally {
lock.unlock();
}
} else {
System.out.println("操作正在进行,请稍后重试");
}
}
}
在 Java 高并发场景下,通过数据库层面和应用层面的多种防重手段,可以有效地避免重复操作带来的问题。开发者应根据具体业务场景和系统架构,选择合适的防重方案,以确保系统的高效、稳定运行。随着技术的不断发展,未来还会有更多创新的解决方案出现,我们需要持续关注并学习,以应对日益复杂的高并发挑战。
猜你喜欢
- 2025-03-20 怒肝15天,终将Kafka的“重平衡”一举拿下
- 2025-03-20 面试官:重写 equals 时为什么一定要重写 hashCode?
- 2025-03-20 面试突击14:方法重写和方法重载有什么区别?
- 2025-03-20 AI大模型企业应用实战(21)-RAG的核心-结果召回和重排序
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)