首先我们可以从两个方面入手,前端验证?,后端验证?

1.前端很简单,我们只需要写个js代码让按钮不可用就行了。
弊端:但是有个弊端就是,如果不是通过页面访问呢?

要明白每个请求都是一个url,而url是可以仿造的,普通的get请求连专业工具都不用,直接浏览器就可以仿造一个url出来,不需 要经过你的按钮点击事件。总结来说,前端验证不安全~

2.那么我们后端验证吧

2.1 后端现在有两种模式,一种是单体模式,这个也比较简单,单体服务器,我们可以通过多线程并发的方式解决

2.2 但是现在,主流的模式其时是分布式

分布式系统网络拓扑结构

不重复订单号 java java防止重复下单_不重复订单号 java

场景描述

秒杀系统提交订单时,由于用户连续快速点击,并且前端没有针对性处理,导致连续发送两次请求,一次命中服务器A,另一次命中服务器B, 那么就生成了两个内容完全相同的订单,只是订单号不同而已.

重复提交的后果

用户在界面看到两个一模一样的订单,不知道应该支付哪个;
系统出现异常数据,影响正常的校验

解决方法

服务器A接收到请求之后,获取锁,获取成功 ,
服务器A进行业务处理,订单提交成功;
服务器B接收到相同的请求,获取锁,失败,
因为锁被服务器A获取了,并且未释放。B 获取锁失败之后,直接返回。
服务器A处理完成,释放锁

使用redis
流程如下:

不重复订单号 java java防止重复下单_加锁_02


下面是代码实现

@Component
public class LockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //加锁的lua脚本
    private String lockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--过期时间 秒\n" +
            "local timeout=tonumber(ARGV[2])\n" +
            "--尝试进行加锁\n" +
            "local flag=redis.call('setnx', lockName, lockValue)\n" +
            "--判断是否获得锁\n" +
            "if flag==1 then\n" +
            "--获得分布式锁,设置过期时间\n" +
            "redis.call('expire', lockName, timeout)\n" +
            "end\n" +
            "--返回标识\n" +
            "return flag ";

    //解锁的lua脚本
    private String unLockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--判断锁是否存在,以及锁的内容是否为自己加的\n" +
            "local value=redis.call('get', lockName)\n" +
            "--判断是否相同\n" +
            "if value == lockValue then\n" +
            "     redis.call('del', lockName)\n" +
            "     return 1\n" +
            "end\n" +
            "return 0";

    private ThreadLocal<String> tokens = new ThreadLocal<>();

    /**
     * 加锁(默认超时时间30s)
     * @return
     */
    public void lock(String lockName){
        lock(lockName, 30);
    }
	/**
     * 加锁可自定义超时时间timeout
     * @return
     */
    public void lock(String lockName, Integer timeout){

        String token = UUID.randomUUID().toString();
        //设置给threadLocal
        tokens.set(token);

        //分布式锁 - 加锁
        Long flag = (Long) redisTemplate.execute(new DefaultRedisScript(lockLua, Long.class),
                Collections.singletonList(lockName),
                token, timeout + ""
        );

        System.out.println("获得锁的结果:" + flag);

        //设置锁的自旋
        if (flag == 0) {
            //未获得锁
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock(lockName, timeout);
        }
    }

    /**
     * 解锁
     * @return
     */
    public boolean unlock(String lockName){

        //获得ThreadLocal
        String token = tokens.get();

        //解锁
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript(unLockLua, Long.class),
                Collections.singletonList(lockName),
                token);

        System.out.println("删除锁的结果:" + result);

        return result == 1;
    }
}

1.需要加锁的地方只需要调用这个工具类中的 lock(锁名A) 方法,里面默认是30秒的过期时间,也可以用重载方法设置过期时间
2.解锁的话调用unlock(锁名A)
这样就可以实现分布式锁

为什么要使用redis

因为关于锁有两个重要的操作:

获取锁;
释放锁.
在分布式环境,必须保证这两个操作是原子性的,
即不能把获取锁分为两步:先查询,再add.
同时,获取锁时,能够设置有效期.

分布式锁实现时要注意的问题

提供锁的服务必须是一个唯一的服务,即负载均衡的n个服务单体访问的是同一个服务;
能够设置锁的有效期,不能让某个消费者永久地持有锁;
能够释放锁;
不同的业务逻辑竞争不同的锁,必须下单和减库存 使用不同的锁.
redis 还能做什么
redis除了可以实现分布式锁,还能作为缓存服务器,
在实现需求中,我经常把一些容易变化的配置放在redis中, 这样当产品经理需求变更时,我只需修改redis,即时生效,不用上线

redis 还可以当做定时器