当需要防止订单重复提交时,可以结合使用Token机制和Redis分布式锁来保证订单的幂等性和防止重复提交
Token机制:前端在订单提交请求中携带一个唯一的Token,后台在处理请求时验证Token的有效性。如果Token有效,则处理订单;如果Token无效,拒绝处理。
// 前端提交订单请求时,携带Token
$.ajax({
type: 'POST',
url: '/submitOrder',
data: {
// ...
token: 'unique_token' // 前端生成或从后台获取的唯一Token
},
success: function(response) {
// 处理响应
}
});
Redis分布式锁:使用Redis作为分布式锁的存储介质,确保只有一个线程可以处理订单提交请求。
// Spring Boot中使用Spring Data Redis操作Redis
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@RequestMapping("/submitOrder")
public String submitOrder(HttpServletRequest request, String token) {
// 构建Redis锁的Key
String lockKey = "order_lock:" + token;
// 使用Redis分布式锁
ValueOperations<String, String> ops = redisTemplate.opsForValue();
Boolean locked = ops.setIfAbsent(lockKey, "locked"); // 尝试获取锁
try {
if (locked != null && locked) {
// 获取锁成功,可以处理订单提交逻辑
// ...
// 订单处理完成后,删除锁,确保下次请求可以再次获取锁
redisTemplate.delete(lockKey);
return "order_success";
} else {
// 获取锁失败,可能是其他线程正在处理相同订单
return "order_failed";
}
} finally {
// 释放锁
if (locked != null && locked) {
redisTemplate.delete(lockKey);
}
}
}
上述示例中,前端提交订单请求时携带了一个唯一的Token。后台使用Redis分布式锁来确保同一Token的订单只能被一个线程处理,从而防止重复提交。当订单处理完成后,释放锁,确保下次请求可以再次获取锁。
这种组合方式可以有效地防止订单的重复提交,同时保证了订单的幂等性,提高了系统的可靠性。在实际应用中,需要根据具体业务场景和性能要求来调整锁的策略,如设置锁的超时时间、处理锁争用等。
那如何设置超时时间,处理锁竞争呢?
设置分布式锁的超时时间和处理锁竞争是为了确保在某些异常情况下(比如持有锁的线程意外终止),不会导致锁一直占用,从而避免潜在的死锁情况。以下是如何设置超时时间和处理锁竞争的示例:
设置锁的超时时间:
可以使用Redis的expire
命令为锁设置一个超时时间,确保即使持有锁的线程在处理订单期间发生了异常,锁最终会在一定时间后自动释放。
// 设置锁的超时时间为30秒
redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);
处理锁竞争:
如果多个线程同时尝试获取同一个锁,只有一个线程能够成功获取锁,其他线程需要等待。为了避免锁竞争导致性能下降,可以使用一些策略,如重试机制或者自旋等待。
- 重试机制:如果获取锁失败,可以在一定的时间间隔后进行重试,直到获取到锁或达到最大重试次数。这可以通过循环来实现。
int maxRetries = 3;
int retryIntervalMillis = 100; // 100毫秒
for (int i = 0; i < maxRetries; i++) {
Boolean locked = ops.setIfAbsent(lockKey, "locked");
if (locked != null && locked) {
// 获取锁成功,处理订单
// ...
return "order_success";
} else {
// 获取锁失败,等待一段时间后重试
Thread.sleep(retryIntervalMillis);
}
}
// 多次尝试后仍无法获取锁,处理失败
return "order_failed";
自旋等待:在获取锁失败时,不立即休眠,而是在循环中不断尝试获取锁,直到成功或达到一定的尝试次数。
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
Boolean locked = ops.setIfAbsent(lockKey, "locked");
if (locked != null && locked) {
// 获取锁成功,处理订单
// ...
return "order_success";
}
}
// 多次尝试后仍无法获取锁,处理失败
return "order_failed";
以上两种方式都可以根据具体需求和性能要求来调整,确保在高并发情况下能够安全地处理订单提交请求。同时,需要注意设置适当的最大重试次数和重试间隔,以防止无限制的重试,降低系统的可用性。
如何设置适当的最大重试次数和重试间隔
设置最大重试次数和重试间隔需要根据具体的业务需求、系统性能和并发情况来进行调整。没有一个固定的最佳值,这些值应该根据实际情况进行优化。我只能给一些关于如何设置最大重试次数和重试间隔的建议:
最大重试次数: 最大重试次数应该根据业务需求和系统容忍的最大延迟来确定。如果允许稍微多一点的延迟,可以设置较大的最大重试次数,以增加获取锁的机会。通常,最大重试次数可以设置为3到5次,但在某些情况下也可以更多。
重试间隔: 重试间隔应该足够小以减小竞争锁的线程之间的延迟,但又不应该过于频繁以避免对Redis服务器造成不必要的负载。通常,重试间隔可以设置为几百毫秒,具体值取决于业务需求和系统负载。
指数退避策略: 一种常见的设置重试间隔的策略是使用指数退避(Exponential Backoff),即在每次重试之后将重试间隔逐渐增加。这有助于减轻锁竞争时的争夺情况,提高系统的稳定性。例如,可以设置初始重试间隔为100毫秒,然后在每次重试时将间隔翻倍。
代码示例展示如何设置设置最大重试次数和指数退避的重试间隔:
int maxRetries = 5;
int initialRetryIntervalMillis = 100; // 初始重试间隔为100毫秒
for (int i = 0; i < maxRetries; i++) {
Boolean locked = ops.setIfAbsent(lockKey, "locked");
if (locked != null && locked) {
// 获取锁成功,处理订单
// ...
return "order_success";
} else {
// 获取锁失败,等待重试
Thread.sleep(initialRetryIntervalMillis * (1 << i)); // 指数退避
}
}
// 多次尝试后仍无法获取锁,处理失败
return "order_failed";