高并发下的防并发实战:C端/B端项目并发控制完全指南
摘要:本文系统梳理C端/B端项目中的防并发方案,涵盖前端防重复提交(按钮禁用、请求去重)和后端并发控制(分布式锁、乐观锁、悲观锁、幂等性、限流、队列削峰)。通过秒杀、支付、批量导入等实战场景,提供完整的代码示例和方案选择指南,适配SpringBoot 2.x和Vue3技术栈。
一、引言
在互联网应用中,并发控制是保障系统稳定性和数据一致性的核心问题。无论是C端用户的秒杀抢购、下单支付,还是B端系统的批量操作、数据同步,都需要有效的防并发机制。本文将从前后端两个维度,系统梳理C端和B端项目中常见的防并发思路和实战方案。
1.1 并发问题的典型场景
C端场景
- 秒杀抢购:库存扣减、订单创建
- 支付下单:重复支付、订单重复创建
- 优惠券领取:重复领取、超发
- 签到打卡:重复签到、数据重复
B端场景
- 批量导入:重复导入、数据覆盖
- 审核流程:重复审核、状态冲突
- 数据同步:重复同步、数据不一致
- 报表生成:重复生成、资源浪费
1.2 并发问题的危害
- 数据不一致:库存超卖、余额错误
- 重复操作:重复下单、重复扣款
- 资源浪费:重复计算、重复调用
- 业务异常:状态错乱、流程中断
二、防并发方案全景图
2.1 防并发方案-思维导图

2.2 方案选择决策树

三、前端防并发方案
3.1 UI层防重复提交
3.1.1 按钮禁用方案
Vue3 实现(Composition API):
vue
<template>
<el-button
:loading="submitting"
:disabled="submitting"
@click="handleSubmit"
>
{{ submitting ? '提交中...' : '提交订单' }}
</el-button>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const submitting = ref(false)
const handleSubmit = async () => {
// 防止重复点击
if (submitting.value) {
return
}
submitting.value = true
try {
await submitOrder()
ElMessage.success('提交成功')
} catch (error) {
ElMessage.error('提交失败:' + error.message)
} finally {
submitting.value = false
}
}
const submitOrder = async () => {
// 调用后端接口
const response = await axios.post('/api/order/submit', orderData)
return response.data
}
</script>
Vue3 实现(Options API):
vue
<template>
<el-button
:loading="submitting"
:disabled="submitting"
@click="handleSubmit"
>
提交订单
</el-button>
</template>
<script>
export default {
data() {
return {
submitting: false
}
},
methods: {
async handleSubmit() {
if (this.submitting) {
return
}
this.submitting = true
try {
await this.submitOrder()
this.$message.success('提交成功')
} catch (error) {
this.$message.error('提交失败:' + error.message)
} finally {
this.submitting = false
}
},
async submitOrder() {
const response = await this.$http.post('/api/order/submit', this.orderData)
return response.data
}
}
}
</script>
3.1.2 防抖(Debounce)方案
适用场景: 搜索框输入、按钮快速点击
vue
<template>
<el-input
v-model="keyword"
@input="handleSearch"
placeholder="请输入关键词"
/>
<el-button @click="handleSubmit">提交</el-button>
</template>
<script setup>
import { ref } from 'vue'
import { debounce } from 'lodash-es'
const keyword = ref('')
// 防抖处理:500ms内只执行最后一次
const handleSearch = debounce((value) => {
console.log('搜索:', value)
// 执行搜索逻辑
searchData(value)
}, 500)
// 按钮防抖
let submitTimer = null
const handleSubmit = () => {
if (submitTimer) {
clearTimeout(submitTimer)
}
submitTimer = setTimeout(() => {
submitOrder()
submitTimer = null
}, 1000) // 1秒内只允许提交一次
}
</script>
自定义防抖指令(Vue3):
javascript
// directives/debounce.js
export default {
mounted(el, binding) {
let timer = null
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
binding.value()
}, binding.arg || 1000)
})
}
}
// 使用
<el-button v-debounce:1000="handleSubmit">提交</el-button>
3.1.3 节流(Throttle)方案
适用场景: 滚动加载、窗口resize
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { throttle } from 'lodash-es'
const handleScroll = throttle(() => {
// 滚动处理逻辑
const scrollTop = document.documentElement.scrollTop
if (scrollTop > 1000) {
loadMore()
}
}, 200) // 200ms内最多执行一次
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
3.2 请求拦截与去重
3.2.1 Axios 请求拦截器
实现请求去重:
javascript
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 存储正在进行的请求
const pendingRequests = new Map()
// 生成请求唯一标识
const generateRequestKey = (config) => {
const { method, url, params, data } = config
return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`
}
// 请求拦截器
axios.interceptors.request.use(
(config) => {
const requestKey = generateRequestKey(config)
// 检查是否有相同的请求正在进行
if (pendingRequests.has(requestKey)) {
// 取消当前请求
config.cancelToken = new axios.CancelToken((cancel) => {
cancel('重复请求已取消')
})
return config
}
// 将请求添加到pending列表
pendingRequests.set(requestKey, config)
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
axios.interceptors.response.use(
(response) => {
const requestKey = generateRequestKey(response.config)
pendingRequests.delete(requestKey)
return response
},
(error) => {
if (error.config) {
const requestKey = generateRequestKey(error.config)
pendingRequests.delete(requestKey)
}
// 处理取消的请求
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message)
return Promise.reject(error)
}
return Promise.reject(error)
}
)
export default axios
3.2.2 基于请求ID的幂等性
前端生成请求ID:
javascript
// utils/requestId.js
import { v4 as uuidv4 } from 'uuid'
// 为每个请求添加唯一ID
axios.interceptors.request.use((config) => {
if (!config.headers['X-Request-Id']) {
config.headers['X-Request-Id'] = uuidv4()
}
return config
})
后端验证请求ID(SpringBoot 2.x):
java
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 请求幂等性拦截器
* 【规范校验】:使用Redis存储请求ID,防止重复请求
* 【版本兼容】:SpringBoot 2.x
*/
@Component
public class IdempotentInterceptor extends HandlerInterceptorAdapter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String REQUEST_ID_PREFIX = "request:id:";
private static final int EXPIRE_SECONDS = 300; // 5分钟过期
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 只拦截POST、PUT、DELETE请求
String method = request.getMethod();
if (!"POST".equals(method) && !"PUT".equals(method) && !"DELETE".equals(method)) {
return true;
}
String requestId = request.getHeader("X-Request-Id");
if (StringUtils.isBlank(requestId)) {
// 没有请求ID,允许通过(兼容旧接口)
return true;
}
String key = REQUEST_ID_PREFIX + requestId;
// 检查请求ID是否已存在
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exists)) {
// 请求重复,返回错误
response.setStatus(HttpStatus.CONFLICT.value());
response.getWriter().write("{\"code\":409,\"message\":\"请求重复\"}");
return false;
}
// 存储请求ID
redisTemplate.opsForValue().set(key, "1", EXPIRE_SECONDS, TimeUnit.SECONDS);
return true;
}
}
3.3 状态管理控制
3.3.1 Vuex/Pinia 状态管理
Vue3 + Pinia 实现:
javascript
// stores/order.js
import { defineStore } from 'pinia'
export const useOrderStore = defineStore('order', {
state: () => ({
submitting: false,
submitTimestamp: 0
}),
actions: {
async submitOrder(orderData) {
// 防止重复提交:5秒内不允许重复提交
const now = Date.now()
if (this.submitting || (now - this.submitTimestamp < 5000)) {
throw new Error('请勿重复提交')
}
this.submitting = true
this.submitTimestamp = now
try {
const response = await axios.post('/api/order/submit', orderData)
return response.data
} finally {
this.submitting = false
}
}
}
})
// 组件中使用
<script setup>
import { useOrderStore } from '@/stores/order'
const orderStore = useOrderStore()
const handleSubmit = async () => {
try {
await orderStore.submitOrder(orderData)
ElMessage.success('提交成功')
} catch (error) {
ElMessage.error(error.message)
}
}
</script>
四、后端防并发方案
4.1 应用层防并发
4.1.1 分布式锁(Redis实现)
完整实现(SpringBoot 2.x):
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis 分布式锁工具类
* 【规范校验】:使用Lua脚本保证原子性,防止误删其他实例的锁
* 【版本兼容】:SpringBoot 2.x + Spring Data Redis 1.7.2
*/
@Component
public class DistributedLockUtil {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
/**
* 尝试获取锁
* @param lockKey 锁的key
* @param lockValue 锁的value(建议使用UUID)
* @param expireSeconds 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String lockValue, int expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁(使用Lua脚本保证原子性)
* @param lockKey 锁的key
* @param lockValue 锁的value
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String lockValue) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
lockValue
);
return result != null && result == 1;
}
/**
* 获取锁(带重试机制)
* @param lockKey 锁的key
* @param expireSeconds 过期时间(秒)
* @param maxWaitSeconds 最大等待时间(秒)
* @return 锁的value,获取失败返回null
*/
public String lockWithRetry(String lockKey, int expireSeconds, int maxWaitSeconds) {
String lockValue = UUID.randomUUID().toString();
long endTime = System.currentTimeMillis() + maxWaitSeconds * 1000;
while (System.currentTimeMillis() < endTime) {
if (tryLock(lockKey, lockValue, expireSeconds)) {
return lockValue;
}
try {
Thread.sleep(100); // 等待100ms后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
return null;
}
}
使用示例:
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 订单服务示例
* 【规范校验】:使用分布式锁防止并发创建订单
* 【版本兼容】:SpringBoot 2.x
*/
@Slf4j
@Service
public class OrderService {
@Autowired
private DistributedLockUtil distributedLockUtil;
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单(防并发)
*/
@Transactional(rollbackFor = Exception.class)
public OrderDTO createOrder(CreateOrderParam param) {
// 构建锁的key:用户ID + 商品ID
String lockKey = String.format("order:create:%s:%s",
param.getUserId(), param.getProductId());
String lockValue = UUID.randomUUID().toString();
int expireSeconds = 30; // 锁30秒过期
// 尝试获取锁
if (!distributedLockUtil.tryLock(lockKey, lockValue, expireSeconds)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"订单创建中,请勿重复提交");
}
try {
// 检查是否已存在订单
OrderPO existingOrder = orderMapper.selectByUserIdAndProductId(
param.getUserId(), param.getProductId());
if (existingOrder != null) {
throw new ServiceException(CommonCode.RECORD_EXISTED.code(),
"订单已存在");
}
// 创建订单
OrderPO order = new OrderPO();
order.setUserId(param.getUserId());
order.setProductId(param.getProductId());
order.setAmount(param.getAmount());
order.setStatus(OrderStatusEnum.PENDING.getCode());
orderMapper.insert(order);
log.info("订单创建成功:orderId={}", order.getId());
return convertToDTO(order);
} finally {
// 释放锁
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
}
4.1.2 本地锁(单机场景)
synchronized 实现:
java
/**
* 本地锁示例(单机部署)
* 【规范校验】:使用synchronized保证线程安全
* 【版本兼容】:JDK 1.8+
*/
@Service
public class LocalLockService {
/**
* 使用synchronized方法锁
*/
public synchronized void processWithMethodLock(String key) {
// 业务逻辑
doBusiness(key);
}
/**
* 使用synchronized代码块锁
*/
private final Object lock = new Object();
public void processWithBlockLock(String key) {
synchronized (lock) {
// 业务逻辑
doBusiness(key);
}
}
/**
* 使用ReentrantLock(更灵活)
*/
private final ReentrantLock reentrantLock = new ReentrantLock();
public void processWithReentrantLock(String key) {
reentrantLock.lock();
try {
// 业务逻辑
doBusiness(key);
} finally {
reentrantLock.unlock();
}
}
}
4.1.3 幂等性设计
基于唯一业务标识的幂等性:
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* 幂等性服务示例
* 【规范校验】:使用Redis + 数据库唯一索引保证幂等性
* 【版本兼容】:SpringBoot 2.x
*/
@Slf4j
@Service
public class IdempotentService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderMapper orderMapper;
private static final String IDEMPOTENT_PREFIX = "idempotent:";
private static final int EXPIRE_SECONDS = 300; // 5分钟
/**
* 幂等性创建订单
* @param param 订单参数
* @param idempotentKey 幂等性key(如:订单号、请求ID)
* @return 订单ID
*/
@Transactional(rollbackFor = Exception.class)
public Long createOrderIdempotent(CreateOrderParam param, String idempotentKey) {
// 1. 检查Redis中是否存在幂等性key
String redisKey = IDEMPOTENT_PREFIX + idempotentKey;
String existingOrderId = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(existingOrderId)) {
log.info("幂等性检查:订单已存在,orderId={}", existingOrderId);
return Long.parseLong(existingOrderId);
}
// 2. 检查数据库中是否存在(双重检查)
OrderPO existingOrder = orderMapper.selectByOrderNo(idempotentKey);
if (existingOrder != null) {
// 回写Redis
redisTemplate.opsForValue().set(
redisKey,
existingOrder.getId().toString(),
EXPIRE_SECONDS,
TimeUnit.SECONDS
);
return existingOrder.getId();
}
// 3. 创建订单
OrderPO order = new OrderPO();
order.setOrderNo(idempotentKey); // 使用幂等性key作为订单号
order.setUserId(param.getUserId());
order.setAmount(param.getAmount());
orderMapper.insert(order);
// 4. 写入Redis
redisTemplate.opsForValue().set(
redisKey,
order.getId().toString(),
EXPIRE_SECONDS,
TimeUnit.SECONDS
);
log.info("订单创建成功:orderId={}, orderNo={}", order.getId(), idempotentKey);
return order.getId();
}
}
幂等性注解实现:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 幂等性注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等性key的生成策略
*/
String keyGenerator() default "default";
/**
* 过期时间(秒)
*/
int expireSeconds() default 300;
}
/**
* 幂等性切面
*/
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 生成幂等性key
String idempotentKey = generateKey(joinPoint, idempotent);
String redisKey = "idempotent:" + idempotentKey;
// 检查是否已执行
String result = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(result)) {
// 已执行,返回缓存结果
return JSON.parseObject(result, getReturnType(joinPoint));
}
// 执行方法
Object returnValue = joinPoint.proceed();
// 缓存结果
redisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(returnValue),
idempotent.expireSeconds(),
TimeUnit.SECONDS
);
return returnValue;
}
}
// 使用示例
@Idempotent(keyGenerator = "orderNo", expireSeconds = 600)
public OrderDTO createOrder(CreateOrderParam param) {
// 创建订单逻辑
}
4.2 数据库层防并发
4.2.1 悲观锁(SELECT FOR UPDATE)
MyBatis实现:
xml
<!-- OrderMapper.xml -->
<select id="selectByIdForUpdate" resultType="OrderPO">
SELECT * FROM t_order
WHERE id = #{id}
FOR UPDATE
</select>
<update id="updateStock">
UPDATE t_product
SET stock = stock - #{quantity}
WHERE id = #{productId}
AND stock >= #{quantity}
</update>
Service层使用:
java
/**
* 使用悲观锁扣减库存
* 【规范校验】:使用SELECT FOR UPDATE保证数据一致性
* 【版本兼容】:SpringBoot 2.x + MyBatis
*/
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(Long productId, Integer quantity) {
// 1. 使用悲观锁查询商品
ProductPO product = productMapper.selectByIdForUpdate(productId);
if (product == null) {
throw new ServiceException(CommonCode.DATA_NOT_FOUND.code(), "商品不存在");
}
// 2. 检查库存
if (product.getStock() < quantity) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(), "库存不足");
}
// 3. 扣减库存
int updateCount = productMapper.updateStock(productId, quantity);
if (updateCount == 0) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(), "库存扣减失败");
}
return true;
}
}
4.2.2 乐观锁(版本号控制)
数据库表设计:
sql
CREATE TABLE t_order (
id BIGINT PRIMARY KEY,
order_no VARCHAR(64) UNIQUE,
user_id BIGINT,
amount DECIMAL(10,2),
status INT,
version INT DEFAULT 0, -- 版本号字段
create_time DATETIME,
update_time DATETIME
);
MyBatis实现:
xml
<!-- OrderMapper.xml -->
<update id="updateWithVersion">
UPDATE t_order
SET
status = #{status},
version = version + 1,
update_time = NOW()
WHERE id = #{id}
AND version = #{version}
</update>
Service层使用:
java
/**
* 使用乐观锁更新订单状态
* 【规范校验】:使用版本号控制,失败时重试
* 【版本兼容】:SpringBoot 2.x
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 更新订单状态(乐观锁)
*/
public boolean updateOrderStatus(Long orderId, Integer newStatus) {
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
// 1. 查询订单(带版本号)
OrderPO order = orderMapper.selectById(orderId);
if (order == null) {
throw new ServiceException(CommonCode.DATA_NOT_FOUND.code(), "订单不存在");
}
// 2. 使用乐观锁更新
int updateCount = orderMapper.updateWithVersion(
orderId,
newStatus,
order.getVersion()
);
if (updateCount > 0) {
log.info("订单状态更新成功:orderId={}, newStatus={}", orderId, newStatus);
return true;
}
// 3. 更新失败,版本冲突,重试
retryCount++;
log.warn("订单状态更新失败(版本冲突),重试:orderId={}, retryCount={}",
orderId, retryCount);
try {
Thread.sleep(100); // 等待100ms后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"订单状态更新失败,请重试");
}
}
4.2.3 唯一索引防重复
数据库设计:
sql
-- 防止重复订单
CREATE UNIQUE INDEX uk_order_user_product ON t_order(user_id, product_id, status);
-- 防止重复支付
CREATE UNIQUE INDEX uk_payment_order ON t_payment(order_id, status);
Service层处理:
java
/**
* 使用唯一索引防止重复数据
* 【规范校验】:数据库唯一索引 + 异常处理
* 【版本兼容】:SpringBoot 2.x
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public Long createOrder(CreateOrderParam param) {
try {
OrderPO order = new OrderPO();
order.setUserId(param.getUserId());
order.setProductId(param.getProductId());
order.setStatus(OrderStatusEnum.PENDING.getCode());
orderMapper.insert(order);
return order.getId();
} catch (DuplicateKeyException e) {
// 唯一索引冲突,说明订单已存在
log.warn("订单已存在:userId={}, productId={}",
param.getUserId(), param.getProductId());
// 查询已存在的订单
OrderPO existingOrder = orderMapper.selectByUserIdAndProductId(
param.getUserId(),
param.getProductId()
);
if (existingOrder != null) {
return existingOrder.getId();
}
throw new ServiceException(CommonCode.RECORD_EXISTED.code(),
"订单已存在");
}
}
}
4.3 限流方案
4.3.1 基于Redis的限流
令牌桶算法实现:
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
/**
* Redis限流工具类(令牌桶算法)
* 【规范校验】:使用Lua脚本保证原子性
* 【版本兼容】:SpringBoot 2.x
*/
@Component
public class RateLimiterUtil {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String TOKEN_BUCKET_SCRIPT =
"local key = KEYS[1] " +
"local capacity = tonumber(ARGV[1]) " +
"local tokens = tonumber(ARGV[2]) " +
"local interval = tonumber(ARGV[3]) " +
"local now = tonumber(ARGV[4]) " +
" " +
"local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill') " +
"local currentTokens = tonumber(bucket[1]) or capacity " +
"local lastRefill = tonumber(bucket[2]) or now " +
" " +
"local elapsed = now - lastRefill " +
"local tokensToAdd = math.floor(elapsed / interval) " +
"currentTokens = math.min(capacity, currentTokens + tokensToAdd) " +
" " +
"if currentTokens >= tokens then " +
" currentTokens = currentTokens - tokens " +
" redis.call('HMSET', key, 'tokens', currentTokens, 'lastRefill', now) " +
" redis.call('EXPIRE', key, 3600) " +
" return 1 " +
"else " +
" redis.call('HMSET', key, 'tokens', currentTokens, 'lastRefill', now) " +
" redis.call('EXPIRE', key, 3600) " +
" return 0 " +
"end";
/**
* 限流检查(令牌桶算法)
* @param key 限流key
* @param capacity 桶容量
* @param tokens 需要的令牌数
* @param interval 令牌生成间隔(毫秒)
* @return 是否允许通过
*/
public boolean tryAcquire(String key, int capacity, int tokens, long interval) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(TOKEN_BUCKET_SCRIPT);
script.setResultType(Long.class);
long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(tokens),
String.valueOf(interval),
String.valueOf(now)
);
return result != null && result == 1;
}
}
使用示例:
java
/**
* 限流使用示例
*/
@Service
public class OrderService {
@Autowired
private RateLimiterUtil rateLimiterUtil;
public OrderDTO createOrder(CreateOrderParam param) {
// 限流:每个用户每秒最多创建1个订单
String limitKey = "rate:limit:order:create:" + param.getUserId();
if (!rateLimiterUtil.tryAcquire(limitKey, 10, 1, 1000)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"请求过于频繁,请稍后再试");
}
// 创建订单逻辑
return doCreateOrder(param);
}
}
4.3.2 滑动窗口限流
java
/**
* 滑动窗口限流
*/
@Component
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 滑动窗口限流
* @param key 限流key
* @param windowSize 窗口大小(秒)
* @param maxRequests 最大请求数
* @return 是否允许通过
*/
public boolean tryAcquire(String key, int windowSize, int maxRequests) {
long now = System.currentTimeMillis();
long windowStart = now - windowSize * 1000;
// 使用ZSet存储请求时间戳
String zsetKey = "rate:limit:sliding:" + key;
// 移除窗口外的数据
redisTemplate.opsForZSet().removeRangeByScore(zsetKey, 0, windowStart);
// 获取当前窗口内的请求数
Long count = redisTemplate.opsForZSet().count(zsetKey, windowStart, now);
if (count != null && count >= maxRequests) {
return false;
}
// 添加当前请求
redisTemplate.opsForZSet().add(zsetKey, String.valueOf(now), now);
redisTemplate.expire(zsetKey, windowSize + 1, TimeUnit.SECONDS);
return true;
}
}
4.4 队列削峰方案
4.4.1 异步队列处理
使用线程池 + 队列:
java
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 订单队列处理
* 【规范校验】:使用队列削峰,防止系统过载
* 【版本兼容】:SpringBoot 2.x
*/
@Component
public class OrderQueueProcessor {
@Autowired
private OrderService orderService;
@Autowired
private ThreadPoolTaskExecutor orderExecutor;
// 订单队列
private final BlockingQueue<CreateOrderParam> orderQueue =
new LinkedBlockingQueue<>(10000);
/**
* 提交订单到队列
*/
public void submitOrder(CreateOrderParam param) {
if (!orderQueue.offer(param)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"系统繁忙,请稍后再试");
}
// 异步处理
orderExecutor.execute(() -> processOrderQueue());
}
/**
* 处理订单队列
*/
private void processOrderQueue() {
while (!orderQueue.isEmpty()) {
try {
CreateOrderParam param = orderQueue.poll();
if (param != null) {
orderService.createOrder(param);
}
} catch (Exception e) {
log.error("订单处理失败", e);
}
}
}
}
五、C端/B端典型场景实战
5.1 C端场景:秒杀抢购
完整方案:
java
/**
* 秒杀服务
* 【场景识别】:高并发秒杀场景
* 【防并发策略】:限流 + 分布式锁 + 数据库乐观锁 + 队列削峰
*/
@Service
public class SeckillService {
@Autowired
private RateLimiterUtil rateLimiterUtil;
@Autowired
private DistributedLockUtil distributedLockUtil;
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderService orderService;
/**
* 秒杀下单
*/
@Transactional(rollbackFor = Exception.class)
public SeckillResult seckill(Long userId, Long productId) {
// 1. 限流:每个用户每秒最多1次请求
String limitKey = "seckill:limit:" + userId;
if (!rateLimiterUtil.tryAcquire(limitKey, 5, 1, 1000)) {
return SeckillResult.fail("请求过于频繁,请稍后再试");
}
// 2. 分布式锁:防止同一用户重复下单
String lockKey = "seckill:lock:" + userId + ":" + productId;
String lockValue = UUID.randomUUID().toString();
if (!distributedLockUtil.tryLock(lockKey, lockValue, 10)) {
return SeckillResult.fail("正在处理中,请勿重复提交");
}
try {
// 3. 检查商品库存(使用乐观锁)
ProductPO product = productMapper.selectById(productId);
if (product == null || product.getStock() <= 0) {
return SeckillResult.fail("商品已售罄");
}
// 4. 扣减库存(乐观锁)
int updateCount = productMapper.updateStockWithVersion(
productId, 1, product.getVersion());
if (updateCount == 0) {
return SeckillResult.fail("库存不足,请重试");
}
// 5. 创建订单(幂等性保证)
String orderNo = generateOrderNo(userId, productId);
Long orderId = orderService.createOrderIdempotent(
buildOrderParam(userId, productId),
orderNo
);
return SeckillResult.success(orderId);
} finally {
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
}
5.2 B端场景:批量导入
完整方案:
java
/**
* 批量导入服务
* 【场景识别】:B端批量数据导入
* 【防并发策略】:分布式锁 + 唯一索引 + 幂等性
*/
@Service
public class BatchImportService {
@Autowired
private DistributedLockUtil distributedLockUtil;
@Autowired
private ImportRecordMapper importRecordMapper;
/**
* 批量导入数据
*/
@Transactional(rollbackFor = Exception.class)
public ImportResult batchImport(Long userId, List<ImportData> dataList, String importBatchNo) {
// 1. 分布式锁:防止重复导入
String lockKey = "import:lock:" + importBatchNo;
String lockValue = UUID.randomUUID().toString();
if (!distributedLockUtil.tryLock(lockKey, lockValue, 300)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"导入任务正在处理中,请勿重复提交");
}
try {
// 2. 检查导入记录是否已存在(幂等性)
ImportRecordPO existingRecord = importRecordMapper.selectByBatchNo(importBatchNo);
if (existingRecord != null) {
return ImportResult.existing(existingRecord.getId());
}
// 3. 创建导入记录
ImportRecordPO record = new ImportRecordPO();
record.setBatchNo(importBatchNo);
record.setUserId(userId);
record.setStatus(ImportStatusEnum.PROCESSING.getCode());
importRecordMapper.insert(record);
// 4. 批量导入数据(使用唯一索引防止重复)
int successCount = 0;
int failCount = 0;
for (ImportData data : dataList) {
try {
importData(data, importBatchNo);
successCount++;
} catch (DuplicateKeyException e) {
// 唯一索引冲突,数据已存在,跳过
log.warn("数据已存在,跳过:{}", data);
successCount++;
} catch (Exception e) {
log.error("数据导入失败", e);
failCount++;
}
}
// 5. 更新导入记录
record.setStatus(ImportStatusEnum.COMPLETED.getCode());
record.setSuccessCount(successCount);
record.setFailCount(failCount);
importRecordMapper.updateById(record);
return ImportResult.success(record.getId(), successCount, failCount);
} finally {
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
private void importData(ImportData data, String importBatchNo) {
// 导入逻辑(数据库唯一索引保证不重复)
DataPO dataPO = convertToPO(data);
dataPO.setImportBatchNo(importBatchNo);
dataMapper.insert(dataPO);
}
}
5.3 支付场景:防止重复支付
完整方案:
java
/**
* 支付服务
* 【场景识别】:支付防重复
* 【防并发策略】:幂等性 + 分布式锁 + 状态机
*/
@Service
public class PaymentService {
@Autowired
private DistributedLockUtil distributedLockUtil;
@Autowired
private PaymentMapper paymentMapper;
/**
* 创建支付订单(防重复)
*/
@Transactional(rollbackFor = Exception.class)
public PaymentDTO createPayment(CreatePaymentParam param) {
// 1. 幂等性检查:使用订单ID作为幂等性key
String idempotentKey = "payment:" + param.getOrderId();
PaymentPO existingPayment = paymentMapper.selectByOrderId(param.getOrderId());
if (existingPayment != null) {
// 支付订单已存在,返回已有订单
return convertToDTO(existingPayment);
}
// 2. 分布式锁:防止并发创建
String lockKey = "payment:lock:" + param.getOrderId();
String lockValue = UUID.randomUUID().toString();
if (!distributedLockUtil.tryLock(lockKey, lockValue, 30)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"支付订单创建中,请勿重复提交");
}
try {
// 3. 双重检查
existingPayment = paymentMapper.selectByOrderId(param.getOrderId());
if (existingPayment != null) {
return convertToDTO(existingPayment);
}
// 4. 创建支付订单
PaymentPO payment = new PaymentPO();
payment.setOrderId(param.getOrderId());
payment.setAmount(param.getAmount());
payment.setStatus(PaymentStatusEnum.PENDING.getCode());
payment.setPaymentNo(generatePaymentNo());
paymentMapper.insert(payment);
return convertToDTO(payment);
} finally {
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
/**
* 支付回调处理(防重复)
*/
@Transactional(rollbackFor = Exception.class)
public void handlePaymentCallback(String paymentNo, String thirdPartyPaymentNo) {
// 1. 查询支付订单
PaymentPO payment = paymentMapper.selectByPaymentNo(paymentNo);
if (payment == null) {
throw new ServiceException(CommonCode.DATA_NOT_FOUND.code(), "支付订单不存在");
}
// 2. 状态检查:只有待支付状态才能处理
if (payment.getStatus() != PaymentStatusEnum.PENDING.getCode()) {
log.warn("支付订单状态异常,跳过处理:paymentNo={}, status={}",
paymentNo, payment.getStatus());
return; // 已处理,直接返回(幂等性)
}
// 3. 分布式锁:防止并发处理
String lockKey = "payment:callback:lock:" + paymentNo;
String lockValue = UUID.randomUUID().toString();
if (!distributedLockUtil.tryLock(lockKey, lockValue, 30)) {
throw new ServiceException(CommonCode.SERVICE_ERROR.code(),
"支付回调处理中,请勿重复提交");
}
try {
// 4. 双重检查状态
payment = paymentMapper.selectByIdForUpdate(payment.getId());
if (payment.getStatus() != PaymentStatusEnum.PENDING.getCode()) {
return; // 已处理
}
// 5. 更新支付状态(乐观锁)
int updateCount = paymentMapper.updateStatusWithVersion(
payment.getId(),
PaymentStatusEnum.SUCCESS.getCode(),
payment.getVersion()
);
if (updateCount > 0) {
// 6. 更新订单状态
orderService.updateOrderStatus(payment.getOrderId(), OrderStatusEnum.PAID.getCode());
log.info("支付回调处理成功:paymentNo={}", paymentNo);
}
} finally {
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
}
六、最佳实践总结
6.1 方案选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 前端防重复提交 | 按钮禁用 + 请求去重 | 用户体验好,实现简单 |
| 单机应用 | 本地锁(synchronized/ReentrantLock) | 性能好,无网络开销 |
| 分布式应用 | 分布式锁(Redis) | 支持集群部署 |
| 高并发读多写少 | 乐观锁 + 版本号 | 性能好,无锁竞争 |
| 高并发写多 | 悲观锁 + 分布式锁 | 保证强一致性 |
| 幂等性要求 | 唯一索引 + Redis幂等性key | 双重保障 |
| 限流需求 | Redis令牌桶/滑动窗口 | 防止系统过载 |
| 削峰需求 | 队列 + 异步处理 | 平滑处理请求 |
6.2 组合使用策略
高并发秒杀场景:
前端:按钮禁用 + 请求去重
↓
后端:限流 → 分布式锁 → 乐观锁 → 幂等性
批量导入场景:
前端:文件上传进度 + 防重复上传
↓
后端:分布式锁 → 唯一索引 → 幂等性
支付场景:
前端:支付按钮禁用 + 请求ID
↓
后端:幂等性 → 分布式锁 → 状态机 → 乐观锁
6.3 注意事项
6.3.1 分布式锁注意事项
- 锁的过期时间:必须大于业务执行时间,避免锁提前释放
- 锁的释放:必须使用Lua脚本保证原子性,防止误删其他实例的锁
- 锁的重入:如需支持重入,使用Redisson等框架
- 死锁预防:设置合理的超时时间,避免死锁
6.3.2 幂等性注意事项
- 幂等性key的选择:使用业务唯一标识(订单号、用户ID+商品ID等)
- 过期时间设置:根据业务特点设置合理的过期时间
- 双重检查:Redis检查 + 数据库检查,保证可靠性
- 异常处理:幂等性失败时的降级策略
6.3.3 数据库锁注意事项
- 悲观锁:必须在事务中使用,避免长时间持有锁
- 乐观锁:失败时需要重试机制
- 唯一索引:合理设计索引,避免性能问题
- 死锁预防:按相同顺序获取锁,避免死锁
6.4 性能优化建议
- 锁的粒度:尽量缩小锁的范围,提高并发性能
- 锁的超时:设置合理的超时时间,避免长时间等待
- 异步处理:非关键路径使用异步处理,提高响应速度
- 缓存优化:合理使用缓存,减少数据库压力
七、总结
7.1 核心要点
- 前端防并发:UI层控制 + 请求拦截,提升用户体验
- 后端防并发:分布式锁 + 数据库锁 + 幂等性,保证数据一致性
- 限流削峰:防止系统过载,保证系统稳定性
- 组合使用:根据场景组合多种方案,达到最佳效果
7.2 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 前端防重复 | 实现简单、用户体验好 | 可被绕过 | 所有场景(第一道防线) |
| 分布式锁 | 支持集群、功能强大 | 有网络开销、需考虑死锁 | 分布式应用 |
| 悲观锁 | 强一致性 | 性能较差、可能死锁 | 写多读少 |
| 乐观锁 | 性能好、无锁竞争 | 需要重试机制 | 读多写少 |
| 幂等性 | 保证不重复执行 | 需要存储空间 | 所有写操作 |
| 限流 | 防止系统过载 | 可能拒绝正常请求 | 高并发场景 |
7.3 SpringBoot 2.x 版本兼容性
| 方案 | 兼容性 | 注意事项 |
|---|---|---|
| 分布式锁(Redis) | ✅ 完全兼容 | 使用Spring Data Redis |
| 数据库锁 | ✅ 完全兼容 | MyBatis支持SELECT FOR UPDATE |
| 乐观锁 | ✅ 完全兼容 | 需要手动实现版本号控制 |
| 限流 | ✅ 完全兼容 | 使用Redis Lua脚本 |
| 队列削峰 | ✅ 完全兼容 | 使用ThreadPoolTaskExecutor |
文档版本: v1.0
适用版本: SpringBoot 2.x、Vue3 3.3+、JDK 1.8+