N

Norlcayn的博客

A text-focused Halo theme

  • 首页
  • 文章
  • 默认分类
  • 关于
主页 订单重复提交问题
文章

订单重复提交问题

发表于 2026-01-16 更新于 昨天
作者 Administrator
18~23 分钟 阅读

订单重复提交

订单重复提交指的是同一个订单多次提交的情况,常见原因如下:

  • 重复点击:用户可能会因为网络延迟、手抖、页面加载缓慢等原因,多次点击下单提交按钮
  • 页面刷新:用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求

防止订单重复提交的本质就是保证下单方法不要被重复调用,这首先可以从前端处理

比如在用户点击提交按钮后,立即将按钮禁用或者改为提交中状态,防止用户再次点击

但是前端只能提高用户的体验,不能保证安全,因为用户可以通过一些脚本提交请求,因此还需要后端处理

后端的处理方式有很多,经常用到的就是Token机制和分布式锁

Token机制

  • 生成Token:用户每次进入下单页面,前端会向服务器请求一个唯一的Token
  • 存储Token:后端将生成Token存放在Redis中,并设置一定的过期时间,然后给前端返回
  • 验证Token:用户提交订单时需要携带Token,服务器验证Token,一旦Token被使用过就失效

whiteboard_exported_image (1)

分布式锁

分布式锁是在分布式系统下的锁机制,旨在保证代码在加锁期间只能被同一个线程访问

实现分布式锁的方案有很多,比如下面这些:

  • MySQL:可以利用MySQL主键、唯一列不能保存重复数据这一特性做分布式锁
  • Redis:可以利用setnx命令无法向Redis重复保存同一key的特性做分布式锁

whiteboard_exported_image (2)

Redisson

Redisson是一个高级的Redis客户端,借助它可以轻松的实现使用Redis做分布式锁、延迟消息队列等功能

相比于RedisTemplate之类的客户端,它更侧重于对Redis功能的模块化封装

API

RedissonClient是Redisson的客户端对象,核心方法如下:

  • 定义锁:getLock(锁)
  • 获取锁:tryLock(获取锁的超时时间, 持有锁的最大时间, 时间单位)
  • 释放锁:unlock()

注意:释放锁的代码一定要写在finally中,保证锁一定可以得到释放

将资料中的demo-redisson.zip解压后导入到Idea中,打开测试类,学习它的核心API

  1. 创建客户端连接对象
@Configuration
public class RedissonConfiguration {
    @Bean
    public RedissonClient redissonClient() {
        // 创建配置类,设置连接地址和密码
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://192.168.101.68:6379")
            .setPassword("redis");

        // 创建客户端
        return Redisson.create(config);
    }
}
  1. 在测试类中测试分布式锁的使用
@SpringBootTest
public class RedissonTest {

    @Autowired
    private RedissonClient redissonClient;

    @Test
    public void test() {
        //模拟启动三个线程去获取锁
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                //定义锁对象,参数就是锁的标识
                RLock lock = redissonClient.getLock("LOCK");
                try {
                    //尝试获取锁对象
                    //参数1: 获取锁的超时时间
                    //参数2: 持有锁的最大时间
                    //参数3: 时间单位
                    //返回结果代表获取锁是否成功
                    boolean flag = lock.tryLock(3, 10, TimeUnit.SECONDS);
                    if (!flag) {
                        throw new RuntimeException("请求太频繁");
                    }
                    //模拟业务逻辑代码
                    System.out.println(Thread.currentThread().getId() + "执行逻辑");
                    //休眠
                    ThreadUtil.sleep(1, TimeUnit.MINUTES);
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getId() + "抢锁失败");
                } finally {
                    lock.unlock();//释放锁
                }
            }).start();
        }

        //不让主线程退出
        ThreadUtil.sleep(30000000);
    }
}

实现防重复下单

@Autowired
private RedissonClient redissonClient;

@GetMapping("/placeOrder")
public void placeOrder(){
    //定义锁对象
    RLock lock = redissonClient.getLock("LOCK");
    try {
        //尝试获取锁对象
        //参数1: 获取锁的超时时间
        //参数2: 持有锁的最大时间
        //参数3: 时间单位
        boolean flag = lock.tryLock(1, 30, TimeUnit.SECONDS);
        if (!flag) {
            throw new RuntimeException("请求太频繁");
        }

        //执行下单业务逻辑,耗时3秒
        System.out.println(Thread.currentThread().getId() + "下单成功");
        ThreadUtil.sleep(3000);

    } catch (Exception e) {
        System.out.println(Thread.currentThread().getId() + "下单失败");
    } finally {
        //不去手动释放锁,代表是让锁等到最大持有时间(30秒)后,自动释放
        //lock.unlock();//释放锁
    }
}

AOP优化代码

上一步的代码虽然可以实现防止重复下单的功能,但是代码繁琐,可以考虑使用AOP的方式来进行优化

当前提供的工程中已经提供好了一个切面类LockAspect,在类中实现的加锁和解锁的逻辑

直接在方法中使用它来简化下单的方法,具体实现如下

@RestController
public class OrderController {

    // formatter: 锁标识
    // waitTime: 获取锁的超时时间,默认单位是秒
    // time: 持有锁的最大时间,默认单位是秒
    // unlock: 方法执行完毕是否需要自动释放锁
    @Lock(formatter = "Lock2", waitTime = 1, time = 30, unlock = false)
    @GetMapping("/placeOrder")
    public void placeOrder() {
        //执行下单业务逻辑,耗时3秒
        System.out.println(Thread.currentThread().getId() + "下单成功");
        ThreadUtil.sleep(3000);
    }
}

AOP:

package com.heima.redisson.aspect;

import com.heima.redisson.annotation.Lock;
import com.heima.redisson.util.AspectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author Mr.M
 * @version 1.0
 * @description 分布式锁工具类
 * @date 2023/7/23 22:56
 */
@Aspect
@Component
public class LockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(lock)")
    public Object handleLock(ProceedingJoinPoint pjp, Lock lock) throws Throwable {
        //锁key的格式化字符,此字符串中有spEL表达式
        String formatter = lock.formatter();
        Method method = AspectUtils.getMethod(pjp);
        Object[] args = pjp.getArgs();
        //得到锁的key
        String redisKey = AspectUtils.parse(formatter, method, args);
        //获取锁阻塞等待的时间,如果是0表示去尝试获取锁,如果获取不到则结束
        long waitTime = 0;
        //阻塞等待获取锁
        if (lock.block()) {
            //根据时间单位转换成ms
            waitTime = lock.waitTime();
        }
        //加锁时长
        long time = lock.time();
        //启动看门狗自动续期
        if (lock.startDog()) {
            time = -1;
            //如果设置自动续期必须在方法执行后释放锁
            if (!lock.unlock()) {
                throw new RuntimeException("请求参数不合法");
            }
        }
        //得到锁对象
        RLock rLock = redissonClient.getLock(redisKey);
        //尝试加锁
        boolean success = rLock.tryLock(waitTime, time, lock.unit());
        if (!success && !lock.block()) {
            //未阻塞要求的情况下未得到锁
            throw new RuntimeException("操作频繁,请稍后重试");
        }
        if (!success) {
            //阻塞情况下未得到锁,请求超时
            throw new RuntimeException("请求超时");
        }

        try {
            return pjp.proceed();
        } finally {
            if (lock.unlock()) {
                rLock.unlock();
            }
        }
    }

}
云岚到家相关技术栈
分布式锁 Redisson
许可协议:  CC BY 4.0
分享

相关文章

1月 16, 2026

状态机

什么是状态机 需求说明 在订单模块中订单共设计了7种状态,如下图 订单会在多个状态间进行转换,这就需要开发人员都去记忆这些转换关系,这是非常麻烦的 if(订单状态 == 待支付){ //如果用户支付成功,执行此场景下的业务逻辑,更新订单状态为派单中 update(id,派单中) ) if(

1月 16, 2026

数据表设计规范

设计范式 数据库设计三范式: 第一范式:数据表中的每一列都是原子的,不可再拆分 第二范式:每张表中的一条记录只描述一个实体 第三范式:数据表中不要存在冗余字段(可以通过其他字段推导出的字段) 1)第一范式:数据表中的每一列都是原子的,不可再拆分 有一张用户考勤表设计如下 id 用户名 考勤时间 1

1月 16, 2026

缓存优化

针对系统中访问频率很高的页面,常见的优化方案如下: 将页面做页面静态化处理 如果静态化之后依旧无法满足需求,可以将静态资源转移到CDN服务器 页面上变动的内容可以异步请求后端服务器获取数据,后端可以添加缓存来减轻数据库的压力 数据库方面可以通过合理的存储引擎选择、表字段设计、索引设计等进一步提高查询

下一篇

数据表设计规范

上一篇

分库分表

最近更新

  • Hello Halo
  • 状态机
  • 数据表设计规范
  • 缓存优化
  • 事务失效

热门标签

Halo 异常处理 Redisson 并行 Vue 并发 递归 请求限流 SpringAMQP 类加载器

目录

©2026 Norlcayn的博客. 保留部分权利。

使用 Halo 主题 Chirpy