防重复提交详解:从前端Vue到后端Java的全面解决方案
防重复提交详解:从前端Vue到后端Java的全面解决方案
一、重复提交问题概述
在Web应用开发中,表单重复提交是一个常见问题,可能导致:
- 数据库中出现重复记录
- 重复执行业务逻辑(如多次扣款)
- 系统资源浪费
- 用户体验下降
本文将从前端Vue和后端Java两个层面,详细介绍防止重复提交的多种解决方案。
二、前端防重复提交(Vue.js)
1. 禁用提交按钮方案
最基本的防重复提交方法是在表单提交后禁用提交按钮,直到请求完成。
案例实现:
<template><div class="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"><h2 class="text-xl font-bold mb-4">方案一:禁用提交按钮</h2><form @submit.prevent="submitForm" class="space-y-4"><div><label for="username" class="block text-sm font-medium mb-1">用户名</label><input id="username" v-model="formData.username" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="email" class="block text-sm font-medium mb-1">邮箱</label><input id="email" v-model="formData.email" type="email" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><button type="submit" class="w-full py-2 px-4 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition-colors":disabled="isSubmitting"><span v-if="isSubmitting">提交中...</span><span v-else>提交</span></button></div><div v-if="message" :class="[success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800', 'p-3 rounded-md']">{{ message }}</div></form></div>
</template><script setup>
import { ref, reactive } from 'vue';const formData = reactive({username: '',email: ''
});const isSubmitting = ref(false);
const message = ref('');
const success = ref(false);async function submitForm() {// 如果已经在提交中,直接返回if (isSubmitting.value) {return;}try {// 设置提交状态为trueisSubmitting.value = true;message.value = '';// 模拟API请求await new Promise(resolve => setTimeout(resolve, 2000));// 请求成功success.value = true;message.value = '表单提交成功!';// 重置表单formData.username = '';formData.email = '';} catch (error) {// 请求失败success.value = false;message.value = '提交失败:' + (error.message || '未知错误');} finally {// 无论成功失败,都将提交状态设为falseisSubmitting.value = false;}
}
</script>
优点:
- 实现简单,适用于大多数场景
- 用户体验良好,提供明确的视觉反馈
缺点:
- 如果用户刷新页面,状态会丢失
- 不能防止用户通过其他方式(如API工具)重复提交
2. 提交状态与加载指示器方案
增强用户体验,添加加载指示器,让用户知道请求正在处理中。
案例实现:
<template><div class="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"><h2 class="text-xl font-bold mb-4">方案二:提交状态与加载指示器</h2><form @submit.prevent="submitForm" class="space-y-4"><div><label for="title" class="block text-sm font-medium mb-1">标题</label><input id="title" v-model="formData.title" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="content" class="block text-sm font-medium mb-1">内容</label><textarea id="content" v-model="formData.content" class="w-full px-3 py-2 border border-gray-300 rounded-md"rows="4"required></textarea></div><div><button type="submit" class="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition-colors relative":disabled="isSubmitting"><span v-if="isSubmitting" class="flex items-center justify-center"><svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>处理中...</span><span v-else>发布文章</span></button></div><div v-if="submitStatus.show" :class="[submitStatus.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800', 'p-3 rounded-md']">{{ submitStatus.message }}</div></form><!-- 全屏加载遮罩 --><div v-if="isSubmitting" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"><div class="bg-white p-6 rounded-lg shadow-lg text-center"><svg class="animate-spin h-10 w-10 text-blue-600 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><p class="text-gray-700">正在提交您的文章,请稍候...</p></div></div></div>
</template><script setup>
import { ref, reactive } from 'vue';const formData = reactive({title: '',content: ''
});const isSubmitting = ref(false);
const submitStatus = reactive({show: false,success: false,message: ''
});async function submitForm() {if (isSubmitting.value) {return;}try {isSubmitting.value = true;submitStatus.show = false;// 模拟API请求await new Promise(resolve => setTimeout(resolve, 3000));// 请求成功submitStatus.success = true;submitStatus.message = '文章发布成功!';submitStatus.show = true;// 重置表单formData.title = '';formData.content = '';} catch (error) {// 请求失败submitStatus.success = false;submitStatus.message = '发布失败:' + (error.message || '服务器错误');submitStatus.show = true;} finally {isSubmitting.value = false;}
}
</script>
优点:
- 提供更丰富的视觉反馈
- 防止用户在请求处理过程中进行其他操作
缺点:
- 仍然不能防止用户刷新页面后重新提交
- 不能防止恶意用户通过其他方式重复提交
3. 表单令牌方案
使用唯一令牌标识每个表单实例,确保同一表单只能提交一次。
案例实现:
<template><div class="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"><h2 class="text-xl font-bold mb-4">方案三:表单令牌</h2><form @submit.prevent="submitForm" class="space-y-4"><div><label for="name" class="block text-sm font-medium mb-1">姓名</label><input id="name" v-model="formData.name" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="phone" class="block text-sm font-medium mb-1">电话</label><input id="phone" v-model="formData.phone" type="tel" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="address" class="block text-sm font-medium mb-1">地址</label><input id="address" v-model="formData.address" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><!-- 隐藏的表单令牌 --><input type="hidden" name="formToken" :value="formToken" /><div><button type="submit" class="w-full py-2 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-md transition-colors":disabled="isSubmitting"><span v-if="isSubmitting" class="flex items-center justify-center"><svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>提交中...</span><span v-else>提交订单</span></button></div><div v-if="resultMessage" :class="[isSuccess ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800', 'p-3 rounded-md']">{{ resultMessage }}</div><div v-if="isTokenUsed" class="p-3 bg-yellow-100 text-yellow-800 rounded-md"><p>检测到此表单已提交过,请勿重复提交!</p><button @click="resetForm" class="mt-2 px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white rounded-md">重置表单</button></div></form></div>
</template><script setup>
import { ref, reactive, onMounted } from 'vue';const formData = reactive({name: '',phone: '',address: ''
});const isSubmitting = ref(false);
const resultMessage = ref('');
const isSuccess = ref(false);
const isTokenUsed = ref(false);
const formToken = ref('');// 生成唯一令牌
function generateToken() {return Date.now().toString(36) + Math.random().toString(36).substring(2);
}// 检查令牌是否已使用
function checkTokenUsed(token) {const usedTokens = JSON.parse(localStorage.getItem('usedFormTokens') || '[]');return usedTokens.includes(token);
}// 标记令牌为已使用
function markTokenAsUsed(token) {const usedTokens = JSON.parse(localStorage.getItem('usedFormTokens') || '[]');usedTokens.push(token);localStorage.setItem('usedFormTokens', JSON.stringify(usedTokens));
}// 重置表单和令牌
function resetForm() {formData.name = '';formData.phone = '';formData.address = '';formToken.value = generateToken();isTokenUsed.value = false;resultMessage.value = '';
}async function submitForm() {// 检查令牌是否已使用if (checkTokenUsed(formToken.value)) {isTokenUsed.value = true;return;}if (isSubmitting.value) {return;}try {isSubmitting.value = true;resultMessage.value = '';// 模拟API请求await new Promise(resolve => setTimeout(resolve, 2000));// 标记令牌为已使用markTokenAsUsed(formToken.value);// 请求成功isSuccess.value = true;resultMessage.value = '订单提交成功!';} catch (error) {// 请求失败isSuccess.value = false;resultMessage.value = '提交失败:' + (error.message || '服务器错误');} finally {isSubmitting.value = false;}
}onMounted(() => {// 组件挂载时生成令牌formToken.value = generateToken();
});
</script>
优点:
- 可以防止同一表单多次提交
- 即使用户刷新页面,也能检测到表单已提交
缺点:
- 本地存储的令牌可能被清除
- 需要后端配合验证令牌
4. 防抖与节流方案
使用防抖(debounce)或节流(throttle)技术防止用户快速多次点击提交按钮。
案例实现:
<template><div class="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"><h2 class="text-xl font-bold mb-4">方案四:防抖与节流</h2><form @submit.prevent class="space-y-4"><div><label for="search" class="block text-sm font-medium mb-1">搜索关键词</label><input id="search" v-model="searchTerm" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"placeholder="输入关键词..."/></div><div class="grid grid-cols-2 gap-4"><div><button @click="normalSubmit"class="w-full py-2 px-4 bg-red-600 hover:bg-red-700 text-white font-medium rounded-md transition-colors">普通提交</button><div class="mt-2 text-xs text-gray-500">点击次数: {{ normalClickCount }}</div></div><div><button @click="debouncedSubmit"class="w-full py-2 px-4 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition-colors">防抖提交</button><div class="mt-2 text-xs text-gray-500">实际提交次数: {{ debounceSubmitCount }}</div></div></div><div class="grid grid-cols-2 gap-4 mt-4"><div><button @click="throttledSubmit"class="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition-colors">节流提交</button><div class="mt-2 text-xs text-gray-500">实际提交次数: {{ throttleSubmitCount }}</div></div><div><button @click="resetCounts"class="w-full py-2 px-4 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-md transition-colors">重置计数</button></div></div><div class="mt-4 p-3 bg-gray-100 rounded-md"><h3 class="font-medium mb-2">日志:</h3><div class="h-40 overflow-y-auto text-sm"><div v-for="(log, index) in logs" :key="index" class="mb-1">{{ log }}</div></div></div></form></div>
</template><script setup>
import { ref, onUnmounted } from 'vue';const searchTerm = ref('');
const normalClickCount = ref(0);
const debounceSubmitCount = ref(0);
const throttleSubmitCount = ref(0);
const logs = ref([]);// 添加日志
function addLog(message) {const now = new Date();const timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`;logs.value.unshift(`[${timeStr}] ${message}`);
}// 普通提交
function normalSubmit() {normalClickCount.value++;addLog(`普通提交被触发,搜索词: ${searchTerm.value}`);
}// 防抖函数
function debounce(func, delay) {let timer = null;return function(...args) {if (timer) clearTimeout(timer);timer = setTimeout(() => {func.apply(this, args);}, delay);};
}// 节流函数
function throttle(func, limit) {let inThrottle = false;return function(...args) {if (!inThrottle) {func.apply(this, args);inThrottle = true;setTimeout(() => {inThrottle = false;}, limit);}};
}// 防抖提交处理函数
function handleDebouncedSubmit() {debounceSubmitCount.value++;addLog(`防抖提交被触发,搜索词: ${searchTerm.value}`);
}// 节流提交处理函数
function handleThrottledSubmit() {throttleSubmitCount.value++;addLog(`节流提交被触发,搜索词: ${searchTerm.value}`);
}// 创建防抖和节流版本的提交函数
const debouncedSubmit = debounce(handleDebouncedSubmit, 1000); // 1秒防抖
const throttledSubmit = throttle(handleThrottledSubmit, 2000); // 2秒节流// 重置计数
function resetCounts() {normalClickCount.value = 0;debounceSubmitCount.value = 0;throttleSubmitCount.value = 0;logs.value = [];addLog('计数已重置');
}// 组件卸载时清除定时器
onUnmounted(() => {// 这里应该清除定时器,但由于我们的防抖和节流函数是闭包形式,// 实际项目中应该使用更完善的实现方式,确保定时器被正确清除
});
</script>
优点:
- 有效防止用户快速多次点击
- 减轻服务器负担
- 适用于搜索、自动保存等场景
缺点:
- 不适用于所有场景,如支付等需要精确控制的操作
- 需要合理设置延迟时间
三、后端防重复提交(Java)
1. 表单令牌验证方案
后端验证前端提交的表单令牌,确保同一令牌只能使用一次。
案例实现:
// Controller层
@RestController
@RequestMapping("/api")
public class FormController {private final FormTokenService tokenService;private final FormService formService;public FormController(FormTokenService tokenService, FormService formService) {this.tokenService = tokenService;this.formService = formService;}@PostMapping("/submit")public ResponseEntity<?> submitForm(@RequestBody FormRequest request,@RequestHeader("X-Form-Token") String token) {// 验证令牌是否有效if (!tokenService.isValidToken(token)) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiResponse(false, "无效的表单令牌"));}// 验证令牌是否已使用if (tokenService.isTokenUsed(token)) {return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(new ApiResponse(false, "表单已提交,请勿重复提交"));}try {// 标记令牌为已使用(在处理业务逻辑前)tokenService.markTokenAsUsed(token);// 处理表单提交String formId = formService.processForm(request);return ResponseEntity.ok(new ApiResponse(true, "表单提交成功", formId));} catch (Exception e) {// 发生异常时,可以选择是否将令牌标记为未使用// tokenService.invalidateToken(token);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "表单提交失败: " + e.getMessage()));}}
}// 令牌服务接口
public interface FormTokenService {boolean isValidToken(String token);boolean isTokenUsed(String token);void markTokenAsUsed(String token);void invalidateToken(String token);
}// 令牌服务实现(使用内存缓存)
@Service
public class FormTokenServiceImpl implements FormTokenService {// 使用Caffeine缓存库private final Cache<String, Boolean> usedTokens;public FormTokenServiceImpl() {// 创建缓存,24小时后过期this.usedTokens = Caffeine.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).maximumSize(10_000).build();}@Overridepublic boolean isValidToken(String token) {// 简单验证:非空且长度合适return token != null && token.length() >= 8;}@Overridepublic boolean isTokenUsed(String token) {return usedTokens.getIfPresent(token) != null;}@Overridepublic void markTokenAsUsed(String token) {usedTokens.put(token, Boolean.TRUE);}@Overridepublic void invalidateToken(String token) {usedTokens.invalidate(token);}
}// 请求和响应类
public class FormRequest {private String name;private String email;private String content;// getters and setters
}public class ApiResponse {private boolean success;private String message;private Object data;public ApiResponse(boolean success, String message) {this.success = success;this.message = message;}public ApiResponse(boolean success, String message, Object data) {this.success = success;this.message = message;this.data = data;}// getters
}
优点:
- 可靠地防止重复提交
- 可以设置令牌过期时间
- 适用于各种表单提交场景
缺点:
- 需要前后端配合
- 缓存管理可能增加系统复杂性
2. 数据库唯一约束方案
利用数据库唯一约束防止重复数据插入。
案例实现:
// 实体类
@Entity
@Table(name = "orders", uniqueConstraints = @UniqueConstraint(columnNames = {"order_number"}))
public class Order {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "order_number", unique = true, nullable = false)private String orderNumber;@Column(name = "customer_name")private String customerName;@Column(name = "amount")private BigDecimal amount;@Column(name = "created_at")private LocalDateTime createdAt;// getters and setters
}// 仓库接口
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {boolean existsByOrderNumber(String orderNumber);
}// 服务实现
@Service
public class OrderServiceImpl implements OrderService {private final OrderRepository orderRepository;public OrderServiceImpl(OrderRepository orderRepository) {this.orderRepository = orderRepository;}@Override@Transactionalpublic String createOrder(OrderRequest request) {// 生成订单号String orderNumber = generateOrderNumber();// 检查订单号是否已存在if (orderRepository.existsByOrderNumber(orderNumber)) {throw new DuplicateOrderException("订单号已存在");}// 创建订单Order order = new Order();order.setOrderNumber(orderNumber);order.setCustomerName(request.getCustomerName());order.setAmount(request.getAmount());order.setCreatedAt(LocalDateTime.now());try {orderRepository.save(order);return orderNumber;} catch (DataIntegrityViolationException e) {// 捕获唯一约束违反异常throw new DuplicateOrderException("创建订单失败,可能是重复提交", e);}}private String generateOrderNumber() {// 生成唯一订单号的逻辑return "ORD" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000));}
}// 控制器
@RestController
@RequestMapping("/api/orders")
public class OrderController {private final OrderService orderService;public OrderController(OrderService orderService) {this.orderService = orderService;}@PostMappingpublic ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {try {String orderNumber = orderService.createOrder(request);return ResponseEntity.ok(new ApiResponse(true, "订单创建成功", orderNumber));} catch (DuplicateOrderException e) {return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(false, e.getMessage()));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "创建订单失败: " + e.getMessage()));}}
}// 异常类
public class DuplicateOrderException extends RuntimeException {public DuplicateOrderException(String message) {super(message);}public DuplicateOrderException(String message, Throwable cause) {super(message, cause);}
}
优点:
- 在数据库层面保证数据唯一性
- 即使应用服务器出现问题,也能保证数据一致性
- 适用于关键业务数据
缺点:
- 只能防止数据重复,不能防止业务逻辑重复执行
- 可能导致用户体验不佳(如果没有适当的错误处理)
3. 事务隔离与锁机制方案
使用数据库事务隔离级别和锁机制防止并发提交。
案例实现:
// 服务实现
@Service
public class PaymentServiceImpl implements PaymentService {private final PaymentRepository paymentRepository;private final AccountRepository accountRepository;public PaymentServiceImpl(PaymentRepository paymentRepository, AccountRepository accountRepository) {this.paymentRepository = paymentRepository;this.accountRepository = accountRepository;}@Override@Transactional(isolation = Isolation.SERIALIZABLE)public String processPayment(PaymentRequest request) {// 检查是否存在相同的支付请求if (paymentRepository.existsByTransactionId(request.getTransactionId())) {throw new DuplicatePaymentException("该交易已处理,请勿重复支付");}// 获取账户(使用悲观锁)Account account = accountRepository.findByIdWithLock(request.getAccountId()).orElseThrow(() -> new AccountNotFoundException("账户不存在"));// 检查余额if (account.getBalance().compareTo(request.getAmount()) < 0) {throw new InsufficientBalanceException("账户余额不足");}// 扣减余额account.setBalance(account.getBalance().subtract(request.getAmount()));accountRepository.save(account);// 创建支付记录Payment payment = new Payment();payment.setTransactionId(request.getTransactionId());payment.setAccountId(request.getAccountId());payment.setAmount(request.getAmount());payment.setStatus("SUCCESS");payment.setCreatedAt(LocalDateTime.now());paymentRepository.save(payment);return payment.getTransactionId();}
}// 仓库接口
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {// 使用悲观锁查询账户@Lock(LockModeType.PESSIMISTIC_WRITE)@Query("SELECT a FROM Account a WHERE a.id = :id")Optional<Account> findByIdWithLock(@Param("id") Long id);
}@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {boolean existsByTransactionId(String transactionId);
}// 控制器
@RestController
@RequestMapping("/api/payments")
public class PaymentController {private final PaymentService paymentService;public PaymentController(PaymentService paymentService) {this.paymentService = paymentService;}@PostMappingpublic ResponseEntity<?> processPayment(@RequestBody PaymentRequest request) {try {String transactionId = paymentService.processPayment(request);return ResponseEntity.ok(new ApiResponse(true, "支付成功", transactionId));} catch (DuplicatePaymentException e) {return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(false, e.getMessage()));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "支付处理失败: " + e.getMessage()));}}
}
优点:
- 可以有效防止并发情况下的重复提交
- 保证数据一致性
- 适用于金融交易等高敏感度场景
缺点:
- 高隔离级别可能影响系统性能
- 锁机制可能导致死锁
- 实现复杂度较高
4. 分布式锁方案
在分布式系统中使用分布式锁防止重复提交。
案例实现(使用Redis实现分布式锁):
// 分布式锁服务接口
public interface DistributedLockService {boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit);void unlock(String lockKey);boolean isLocked(String lockKey);
}// Redis实现的分布式锁服务
@Service
public class RedisDistributedLockService implements DistributedLockService {private final RedissonClient redissonClient;public RedisDistributedLockService(RedissonClient redissonClient) {this.redissonClient = redissonClient;}@Overridepublic boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {RLock lock = redissonClient.getLock(lockKey);try {return lock.tryLock(waitTime, leaseTime, unit);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}@Overridepublic void unlock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);if (lock.isHeldByCurrentThread()) {lock.unlock();}}@Overridepublic boolean isLocked(String lockKey) {RLock lock = redissonClient.getLock(lockKey);return lock.isLocked();}
}// 使用分布式锁的服务实现
@Service
public class RegistrationServiceImpl implements RegistrationService {private final DistributedLockService lockService;private final UserRepository userRepository;public RegistrationServiceImpl(DistributedLockService lockService,UserRepository userRepository) {this.lockService = lockService;this.userRepository = userRepository;}@Overridepublic String registerUser(UserRegistrationRequest request) {// 创建锁键(基于用户名或邮箱)String lockKey = "user_registration:" + request.getEmail();boolean locked = false;try {// 尝试获取锁,等待5秒,锁定30秒locked = lockService.tryLock(lockKey, 5, 30, TimeUnit.SECONDS);if (!locked) {throw new ConcurrentOperationException("操作正在处理中,请稍后再试");}// 检查用户是否已存在if (userRepository.existsByEmail(request.getEmail())) {throw new DuplicateUserException("该邮箱已注册");}// 创建用户User user = new User();user.setUsername(request.getUsername());user.setEmail(request.getEmail());user.setPassword(encryptPassword(request.getPassword()));user.setCreatedAt(LocalDateTime.now());userRepository.save(user);return user.getId().toString();} finally {// 释放锁if (locked) {lockService.unlock(lockKey);}}}private String encryptPassword(String password) {// 密码加密逻辑return BCrypt.hashpw(password, BCrypt.gensalt());}
}// 控制器
@RestController
@RequestMapping("/api/users")
public class UserController {private final RegistrationService registrationService;public UserController(RegistrationService registrationService) {this.registrationService = registrationService;}@PostMapping("/register")public ResponseEntity<?> registerUser(@RequestBody UserRegistrationRequest request) {try {String userId = registrationService.registerUser(request);return ResponseEntity.ok(new ApiResponse(true, "用户注册成功", userId));} catch (DuplicateUserException e) {return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(false, e.getMessage()));} catch (ConcurrentOperationException e) {return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(new ApiResponse(false, e.getMessage()));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "注册失败: " + e.getMessage()));}}
}
优点:
- 适用于分布式系统环境
- 可以跨服务器防止重复提交
- 灵活的锁定策略
缺点:
- 依赖外部系统(如Redis)
- 实现复杂度高
- 需要处理锁超时和失效情况
四、前后端结合的完整解决方案
完整案例:订单提交系统
下面是一个结合前端Vue和后端Java的完整订单提交系统,综合运用多种防重复提交技术。
前端实现(Vue.js):
<template><div class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md"><h1 class="text-2xl font-bold mb-6 text-gray-800">订单提交系统</h1><form @submit.prevent="submitOrder" class="space-y-6"><!-- 客户信息 --><div class="bg-gray-50 p-4 rounded-md"><h2 class="text-lg font-medium mb-3">客户信息</h2><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div><label for="customerName" class="block text-sm font-medium mb-1">客户姓名</label><input id="customerName" v-model="orderData.customerName" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="phone" class="block text-sm font-medium mb-1">联系电话</label><input id="phone" v-model="orderData.phone" type="tel" class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div></div></div><!-- 订单信息 --><div class="bg-gray-50 p-4 rounded-md"><h2 class="text-lg font-medium mb-3">订单信息</h2><div class="space-y-4"><div><label for="productId" class="block text-sm font-medium mb-1">产品选择</label><select id="productId" v-model="orderData.productId" class="w-full px-3 py-2 border border-gray-300 rounded-md"required><option value="">请选择产品</option><option value="1">产品A - ¥100</option><option value="2">产品B - ¥200</option><option value="3">产品C - ¥300</option></select></div><div><label for="quantity" class="block text-sm font-medium mb-1">数量</label><input id="quantity" v-model.number="orderData.quantity" type="number" min="1"class="w-full px-3 py-2 border border-gray-300 rounded-md"required/></div><div><label for="address" class="block text-sm font-medium mb-1">收货地址</label><textarea id="address" v-model="orderData.address" class="w-full px-3 py-2 border border-gray-300 rounded-md"rows="2"required></textarea></div></div></div><!-- 订单摘要 --><div class="bg-gray-50 p-4 rounded-md"><h2 class="text-lg font-medium mb-3">订单摘要</h2><div class="flex justify-between mb-2"><span>产品价格:</span><span>¥{{ productPrice }}</span></div><div class="flex justify-between mb-2"><span>数量:</span><span>{{ orderData.quantity || 0 }}</span></div><div class="flex justify-between font-bold"><span>总计:</span><span>¥{{ totalPrice }}</span></div></div><!-- 隐藏的表单令牌 --><input type="hidden" name="orderToken" :value="orderToken" /><!-- 提交按钮 --><div><button type="submit" class="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition-colors":disabled="isSubmitting || isOrderSubmitted"><span v-if="isSubmitting" class="flex items-center justify-center"><svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>订单提交中...</span><span v-else-if="isOrderSubmitted">订单已提交</span><span v-else>提交订单</span></button></div><!-- 结果消息 --><div v-if="resultMessage" :class="[isSuccess ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800', 'p-4 rounded-md']"><p class="font-medium">{{ resultMessage }}</p><p v-if="orderNumber" class="mt-2">订单号: <span class="font-mono font-bold">{{ orderNumber }}</span></p></div></form><!-- 确认对话框 --><div v-if="showConfirmDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"><div class="bg-white p-6 rounded-lg shadow-lg max-w-md w-full"><h3 class="text-xl font-bold mb-4">确认提交订单</h3><p class="mb-4">您确定要提交此订单吗?提交后将无法修改。</p><div class="flex justify-end space-x-4"><button @click="showConfirmDialog = false" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md">取消</button><button @click="confirmSubmit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md">确认提交</button></div></div></div></div>
</template><script setup>
import { ref, reactive, computed, onMounted } from 'vue';// 订单数据
const orderData = reactive({customerName: '',phone: '',productId: '',quantity: 1,address: ''
});// 状态变量
const isSubmitting = ref(false);
const isOrderSubmitted = ref(false);
const resultMessage = ref('');
const isSuccess = ref(false);
const orderNumber = ref('');
const orderToken = ref('');
const showConfirmDialog = ref(false);// 计算属性
const productPrice = computed(() => {switch (orderData.productId) {case '1': return 100;case '2': return 200;case '3': return 300;default: return 0;}
});const totalPrice = computed(() => {return productPrice.value * (orderData.quantity || 0);
});// 生成唯一令牌
function generateToken() {return Date.now().toString(36) + Math.random().toString(36).substring(2);
}// 防抖函数
function debounce(func, delay) {let timer = null;return function(...args) {if (timer) clearTimeout(timer);timer = setTimeout(() => {func.apply(this, args);}, delay);};
}// 提交订单(显示确认对话框)
function submitOrder() {// 如果已提交或正在提交,直接返回if (isSubmitting.value || isOrderSubmitted.value) {return;}// 显示确认对话框showConfirmDialog.value = true;
}// 确认提交(实际提交逻辑)
const confirmSubmit = debounce(async function() {showConfirmDialog.value = false;if (isSubmitting.value || isOrderSubmitted.value) {return;}try {isSubmitting.value = true;resultMessage.value = '';// 准备提交数据const payload = {...orderData,totalPrice: totalPrice.value,_token: orderToken.value};// 发送到后端const response = await fetch('/api/orders', {method: 'POST',headers: {'Content-Type': 'application/json','X-Order-Token': orderToken.value},body: JSON.stringify(payload)});const data = await response.json();if (!response.ok) {throw new Error(data.message || '订单提交失败');}// 提交成功isSuccess.value = true;resultMessage.value = '订单提交成功!';orderNumber.value = data.data; // 订单号isOrderSubmitted.value = true;// 生成新令牌(以防用户想再次提交)orderToken.value = generateToken();} catch (error) {// 提交失败isSuccess.value = false;resultMessage.value = error.message;} finally {isSubmitting.value = false;}
}, 300);onMounted(() => {// 组件挂载时生成令牌orderToken.value = generateToken();
});
</script>
后端实现(Java Spring Boot):
// 订单实体
@Entity
@Table(name = "orders", uniqueConstraints = @UniqueConstraint(columnNames = {"order_number"}))
public class Order {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "order_number", unique = true, nullable = false)private String orderNumber;@Column(name = "customer_name")private String customerName;@Column(name = "phone")private String phone;@Column(name = "product_id")private Long productId;@Column(name = "quantity")private Integer quantity;@Column(name = "address")private String address;@Column(name = "total_price")private BigDecimal totalPrice;@Column(name = "status")private String status;@Column(name = "created_at")private LocalDateTime createdAt;// getters and setters
}// 订单服务接口
public interface OrderService {String createOrder(OrderRequest request);
}// 订单服务实现
@Service
@Transactional
public class OrderServiceImpl implements OrderService {private final OrderRepository orderRepository;private final OrderTokenService tokenService;public OrderServiceImpl(OrderRepository orderRepository, OrderTokenService tokenService) {this.orderRepository = orderRepository;this.tokenService = tokenService;}@Override@Transactional(isolation = Isolation.SERIALIZABLE)public String createOrder(OrderRequest request) {// 验证令牌String token = request.getToken();if (tokenService.isTokenUsed(token)) {throw new DuplicateOrderException("订单已提交,请勿重复提交");}try {// 标记令牌为已使用tokenService.markTokenAsUsed(token);// 生成订单号String orderNumber = generateOrderNumber();// 创建订单Order order = new Order();order.setOrderNumber(orderNumber);order.setCustomerName(request.getCustomerName());order.setPhone(request.getPhone());order.setProductId(request.getProductId());order.setQuantity(request.getQuantity());order.setAddress(request.getAddress());order.setTotalPrice(request.getTotalPrice());order.setStatus("PENDING");order.setCreatedAt(LocalDateTime.now());orderRepository.save(order);// 异步处理订单(示例)processOrderAsync(order);return orderNumber;} catch (DataIntegrityViolationException e) {// 捕获数据库唯一约束异常throw new DuplicateOrderException("订单创建失败,可能是重复提交", e);}}private String generateOrderNumber() {return "ORD" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + String.format("%04d", new Random().nextInt(10000));}@Asyncpublic void processOrderAsync(Order order) {// 异步处理订单的逻辑try {// 模拟处理时间Thread.sleep(5000);// 更新订单状态order.setStatus("PROCESSED");orderRepository.save(order);} catch (Exception e) {// 处理异常order.setStatus("ERROR");orderRepository.save(order);}}
}// 令牌服务实现
@Service
public class OrderTokenServiceImpl implements OrderTokenService {private final RedisTemplate<String, Boolean> redisTemplate;public OrderTokenServiceImpl(RedisTemplate<String, Boolean> redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic boolean isTokenUsed(String token) {Boolean used = redisTemplate.opsForValue().get("order_token:" + token);return used != null && used;}@Overridepublic void markTokenAsUsed(String token) {redisTemplate.opsForValue().set("order_token:" + token, true, 24, TimeUnit.HOURS);}@Overridepublic void invalidateToken(String token) {redisTemplate.delete("order_token:" + token);}
}// 控制器
@RestController
@RequestMapping("/api/orders")
public class OrderController {private final OrderService orderService;private static final Logger logger = LoggerFactory.getLogger(OrderController.class);public OrderController(OrderService orderService) {this.orderService = orderService;}@PostMappingpublic ResponseEntity<?> createOrder(@RequestBody OrderRequest request,@RequestHeader("X-Order-Token") String token) {// 设置令牌(以防请求体中没有)request.setToken(token);try {// 记录请求日志logger.info("Received order request with token: {}", token);// 创建订单String orderNumber = orderService.createOrder(request);// 记录成功日志logger.info("Order created successfully: {}", orderNumber);return ResponseEntity.ok(new ApiResponse(true, "订单提交成功", orderNumber));} catch (DuplicateOrderException e) {// 记录重复提交日志logger.warn("Duplicate order submission: {}", e.getMessage());return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(false, e.getMessage()));} catch (Exception e) {// 记录错误日志logger.error("Error creating order", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "订单提交失败: " + e.getMessage()));}}
}
五、最佳实践与总结
最佳实践
-
多层防护:
-
前端:禁用按钮 + 视觉反馈 + 表单令牌
-
后端:令牌验证 + 数据库约束 + 事务隔离
-
分布式系统:分布式锁 + 幂等性设计
-
前端防护:
-
禁用提交按钮,防止用户多次点击
-
提供明确的加载状态反馈
-
使用防抖/节流限制快速点击
-
添加确认对话框增加用户确认步骤
-
生成并使用表单令牌
-
后端防护:
-
验证前端提交的令牌
-
使用数据库唯一约束
-
选择合适的事务隔离级别
-
实现幂等性API设计
-
使用分布式锁(在分布式系统中)
-
记录详细日志,便于问题排查
-
异常处理:
-
前端友好展示错误信息
-
后端返回明确的错误状态码和信息
-
区分不同类型的错误(如重复提交、服务器错误等)
-
性能考虑:
-
避免过度使用高隔离级别事务
-
合理设置锁超时时间
-
使用异步处理长时间运行的任务
总结
防止表单重复提交是Web应用开发中的重要环节,需要前后端协同配合。本文详细介绍了多种防重复提交的解决方案:
-
前端Vue.js解决方案:
-
禁用提交按钮
-
提交状态与加载指示器
-
表单令牌
-
防抖与节流
-
后端Java解决方案:
-
表单令牌验证
-
数据库唯一约束
-
事务隔离与锁机制
-
分布式锁
-
综合解决方案:
-
结合前后端多种技术
-
多层次防护机制
-
完善的异常处理
-
良好的用户体验
通过合理选择和组合这些技术,可以有效防止表单重复提交问题,保证系统数据一致性和用户体验。在实际应用中,应根据业务场景和系统架构选择最适合的解决方案。
相关文章:
防重复提交详解:从前端Vue到后端Java的全面解决方案
防重复提交详解:从前端Vue到后端Java的全面解决方案 一、重复提交问题概述 在Web应用开发中,表单重复提交是一个常见问题,可能导致: 数据库中出现重复记录重复执行业务逻辑(如多次扣款)系统资源浪费用户…...
同一子网通信
添加交换机后的通信流程 1. 同一子网内(使用交换机) 判断是否在同一子网: 主机A通过子网掩码判断主机B的IP地址是否属于同一子网。若在同一子网,主机A需要通过ARP获取主机B的MAC地址。 ARP请求(广播)&…...
快速迭代:利用 nodemon 和其他工具实现 Express.js 热更新
在开发 Express.js 应用时,热更新(Hot Reloading)可以显著提升开发效率,因为它允许你在修改代码后立即看到效果,而无需手动重启服务器。以下是几种实现热更新的方法和工具,帮助你在开发过程中更高效地工作。…...
BUG日志:Maven项目启动报错(文件名或者文件扩展名过长)
Bug日志编号:[Maven-001] 标题:Windows系统下Maven项目因路径过长导致命令行执行失败 1. 问题描述 现象:执行mvn clean install时报错: The input line is too long 或 The filename or extension is too long触发条件…...
IntelliJ IDEA 快捷键系列:重命名快捷键详解
目录 引言一、默认重命名快捷键1. Windows 系统2. Mac 系统 二、操作步骤与技巧1. 精准选择重命名范围2. 智能过滤无关内容 三、总结 引言 在代码重构中,重命名变量、类、方法 是最常用的操作之一。正确使用快捷键可以极大提升开发效率。本文针对 Ma…...
零基础掌握分布式ID生成:从理论到实战的完整指南 [特殊字符]
一、为什么需要分布式ID? 🤔 在单机系统中,使用数据库自增ID就能满足需求。但在分布式系统中,多个服务节点同时生成ID时会出现以下问题: ID冲突:不同节点生成相同ID 扩展困难:数据库自增ID无法…...
使用python反射,实现pytest读取yaml并发送请求
pytest yaml yaml - feature: 用户模块story: 登录title: 添加用户request:method: POSTurl: /system/user/listheaders: nullparams: nullvalidate: nullread_yaml_all def read_yaml_all(path):with open(path, r, encodingutf-8) as f:value yaml.safe_load(f)return v…...
点灯、点各式各样的灯
鱼离水则身枯,心离书则神索。 前言闪灯呼吸灯流水灯二进制数显示灯蜂鸣器节拍流水音乐会总结 前言 上回书咱们简单了解了一点有关特殊功能寄存器sfr、通用输入输出GPIO、位操作运算符sbit和一个不靠单片机上的晶振(拿来定时的)的依托于单片机CPU空操作的ms级延时函…...
Matlab 汽车悬架系统动力学建模与仿真
1、内容简介 略 Matlab 170-汽车悬架系统动力学建模与仿真 可以交流、咨询、答疑 2、内容说明 略 本文对题目给定的1/2汽车四自由度模型,建立状态空间模型进行系统分析,并通过MATLAB仿真对系统进行稳定性、可控可观测性分析,对得的结果进行…...
专访数势科技谭李:智能分析 Agent 打通数据平权的最后一公里
作者|斗斗 编辑|皮爷 出品|产业家 伦敦塔桥下的泰晤士河底,埋藏着工业革命的隐秘图腾——布鲁内尔设计的隧道盾构机。在19世纪城市地下轨道建设的过程中,这个直径11米的钢铁巨兽没有选择拓宽河道,而是开创了地下通行的新维度。 “我们不…...
了解浏览器
本文来自腾讯元宝 Chrome浏览器(Google Chrome)是由Google开发的一款免费网页浏览器,自2008年发布以来凭借其高效、安全、简洁的特点成为全球市场份额最高的浏览器。以下是其核心信息及最新动态的综合分析: 一、核心优势与技术特点…...
2、操作系统之软件基础
一、硬件支持系统 ,系统管理硬件 操作系统核心功能可以分为: 守护者:对硬件和软件资源的管理协调者:通过机制,将各种各样的硬件资源适配给软件使用。 所以为了更好的管理硬件,操作系统引进了软件。其中3大…...
STC89C52单片机学习——第20节: [8-2]串口向电脑发送数据电脑通过串口控制LED
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.03.15 51单片机学习——第20节: [8-2]串口向电脑发送数据&电脑通过串口控制LED 前言…...
K8S下nodelocaldns crash问题导致域名请求响应缓慢
前言 最近做项目,有业务出现偶发的部署导致响应很慢的情况,据了解,业务使用域名访问,相同的nginx代理,唯一的区别就是K8S重新部署了。那么问题大概率出现在容器平台,毕竟业务是重启几次正常,偶…...
CVPR2024 | TT3D | 物理世界中可迁移目标性 3D 对抗攻击
Towards Transferable Targeted 3D Adversarial Attack in the Physical World 速览总结摘要-Abstract引言-Introduction相关工作-Related Work方法-MethodologyPreliminray-预备知识问题表述-Problem FormulationNeRF参数空间中的双重优化-Dual Optimization in NeRF Paramete…...
全面对比分析:HDMI、DP、DVI、VGA、Type-C、SDI视频接口特点详解
在当今的多媒体时代,视频接口的选择对于设备连接和显示效果至关重要。不同的视频接口在传输质量、兼容性、带宽等方面各有优劣。本文将全面对比分析常用的视频接口HDMI、DP、DVI、VGA、Type-C、SDI,帮助读者更好地理解它们的特点和适用场景。 一、HDMI&…...
传输层自学
传输实体:完成传输层任务的硬件或软件 可能位于: 操作系统内核独立的用户进程绑定在网络应用中的链接库网络接口卡 1.功能: 网络层与传输层作用范围比较? 网络层负责把数据从源机送达到目的机 传输层负责把数据送达到具体的应…...
使用爬虫获取自定义API操作API接口
1. 引言 在现代Web开发中,API(应用程序接口)是前后端通信的桥梁。通过API,前端可以从后端获取数据,进行各种操作。而爬虫是一种自动化工具,用于从网站上提取数据。本文将详细介绍如何使用爬虫获取自定义AP…...
微服务架构下前端如何配置 OpenAPI 接口
在微服务架构中,后端通常由多个独立的服务组成,每个服务可能提供自己的 API 接口。为了在前端项目中高效地调用这些 API,可以使用 OpenAPI 规范生成客户端代码。以下是详细的配置步骤和最佳实践: 1. 理解 OpenAPI 规范 OpenAPI 是…...
Kotlin知识体系(二) : Kotlin的七个关键特性
前言 在Android开发中,Kotlin以其简洁的语法和强大的特性显著提升了开发效率。本文将解析Kotlin中7个关键特性,通过代码示例展示它们在实际开发中的应用。 一、构造函数:主次分明 主构造函数 class User constructor(_name: String) { //…...
FreeRTOS源码概述
FreeRTOS源码概述 1 FreeRTOS目录结构 使用 STM32CubeMX 创建的 FreeRTOS 工程中,FreeRTOS 相关的源码如下: 主要涉及2个目录: Core Inc 目录下的 FreeRTOSConfig.h 是配置文件Src 目录下的 freertos.c 是 STM32CubeMX 创建的默认任务 Mi…...
日志统计(C++,模拟,双指针)
题目要我们求在某个时间段中,帖子点赞数达到K的帖子数 遍历方式一 我们可以先对所有帖子根据时间,升序排序 枚举每一条帖子,枚举后续每一条帖子,如果id相同且时间差小于d,那么就记录起来,如果记录数量cn…...
加固脱壳技术:DEX动态加载对抗
1. 加固技术原理剖析 1.1 DEX保护演进路线 加固方案发展历程: graph LR A[2015 代码混淆] --> B[2017 DEX动态加载] B --> C[2019 VMP指令虚拟化] C --> D[2022 全链路加密] 1.1.1 主流加固方案对比 厂商核心防护技术弱点分析梆梆加固DEX文件分片…...
C++之list类(超详细)
在上一节中我们学习了STL中的vector这个容器,这节我们来学习一下另外一个常用的容器——list。 文章目录 前言 一、list的介绍 二、list的使用及相关接口 1.list的使用 2.list的迭代器使用 3.list的相关接口 3.1 list capacity 3.2 list element access 3.3…...
强化学习的一些概念
目录 强化学习 打个比方 核心要素 State Action Reward 几个代码demo 学习目标 强化学习 强化学习(Reinforcement Learning, RL)是机器学习的一个分支,旨在让智能体(Agent)通过与环境的交互学习最优策略,以…...
MambaTab:表格数据处理的新利器
——基于结构化状态空间模型的特征增量学习框架 摘要 本文提出MambaTab,一种基于结构化状态空间模型(SSM)的表格数据处理框架。通过创新的嵌入稳定化设计与轻量化SSM架构,MambaTab在普通监督学习和特征增量学习场景中均表现优异&…...
Kafka的流量控制机制
Kafka的流量控制机制 Kafka 作为一款高吞吐量的消息队列系统,能够在海量数据场景下提供稳定的消息生产和消费能力,其背后的流量控制机制功不可没。我们需要认识到,Kafka 的流量控制并非仅仅是为了防止系统过载或崩溃,它的目标是实…...
RabbitMQ支持的复杂的消息交换模式
RabbitMQ支持多种复杂的消息交换模式,这些模式通过不同的交换机类型和队列特性实现,能够满足多样化的业务需求。以下是RabbitMQ支持的主要复杂消息交换模式: 1. Direct Exchange(直连交换机) 直连交换机根据消息的路由…...
CSSHTML新特性
HTML5 新特性探秘 在 Web 开发的不断演进中,HTML5 带来了一系列令人振奋的新特性,极大地提升了网页的功能和用户体验。今天,我们就来深入探究一下这些新特性。 语义化标签:让网页结构更清晰 语义化标签是 HTML5 的一大亮点。在…...
51单片机的工作方式
目录 一、51 单片机的时钟电路及时钟信号 (一)时钟电路 (二)时钟信号 二、51 单片机的CPU 时序 (一)时钟周期 (二)机器周期 (三)指令周期 三、…...
