首先我们可以从两个方面入手,前端验证?,后端验证?
1.前端很简单,我们只需要写个js代码让按钮不可用就行了。
弊端:但是有个弊端就是,如果不是通过页面访问呢?
要明白每个请求都是一个url,而url是可以仿造的,普通的get请求连专业工具都不用,直接浏览器就可以仿造一个url出来,不需 要经过你的按钮点击事件。总结来说,前端验证不安全~
2.那么我们后端验证吧
2.1 后端现在有两种模式,一种是单体模式,这个也比较简单,单体服务器,我们可以通过多线程并发的方式解决
2.2 但是现在,主流的模式其时是分布式
分布式系统网络拓扑结构
场景描述
秒杀系统提交订单时,由于用户连续快速点击,并且前端没有针对性处理,导致连续发送两次请求,一次命中服务器A,另一次命中服务器B, 那么就生成了两个内容完全相同的订单,只是订单号不同而已.
重复提交的后果
用户在界面看到两个一模一样的订单,不知道应该支付哪个;
系统出现异常数据,影响正常的校验
解决方法
服务器A接收到请求之后,获取锁,获取成功 ,
服务器A进行业务处理,订单提交成功;
服务器B接收到相同的请求,获取锁,失败,
因为锁被服务器A获取了,并且未释放。B 获取锁失败之后,直接返回。
服务器A处理完成,释放锁
使用redis
流程如下:
下面是代码实现
@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 还可以当做定时器