上一篇文章中我们大概了解了Curator做读写锁的原理和过程。根据了解,我们可以使用curator的读写锁来做一个分布式防重复提交的策略。为什么采用curator来做这个事情的原因是curator提供的读写锁能够跨线程和jvm进行加锁。如果不加锁,那么因为网络抖动或者线程切换,谁都不知道防重复提交的token标志是否被其他请求修改。因此这块必然要采用加锁的方式。通过锁的创建和删除来保持多个重复请求的有序性,在保证有序性之后,我们就可以按照逻辑对token进行修改,这样其他线程就能够判断自身是否为重复请求。除此之外,在加锁的时候我们采用临时znode,在会话结束之后就可以自动销毁。因此可以避免zk服务端被累计打满的情况。当然这块的会话时间是可以根据业务需求设置的。对于放重复提交的一般规则来说,我无非就是将session提取出来,而session则是和用户绑定的,因此这块我们将userId作为放重复提交的判断标志,将token表示该用户下次提交的表单的有效token,因此同一时刻,只允许同一用户提交一个表单,否则就会因为抢占token,而导致后一表单提交被认定为重复的提交(这块需要优化,下一个版本再优化!)。
以下为作者编写的相关代码。
代码语言:javascript复制@Configuration
public class TestCurd {
@Bean
public CuratorFramework main() {
// 每3秒重连一次,重连3次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//创建连接对象
CuratorFramework client = CuratorFrameworkFactory.builder()
//IP地址端口号
.connectString("127.0.0.1:2181")
//客户端与服务器之间的会话超时时间
.sessionTimeoutMs(10000)
//当客户端与服务器之间会话超时3s后,进行一次重连
.retryPolicy(retryPolicy)
//命名空间,当我们创建节点的时候,以/create为父节点
.namespace("create")
//构建连接对象
.build();
//打开连接
client.start();
//是否成功建立连接,true :建立, false:没有建立
System.out.println(client.isStarted());
return client;
}
}
主要逻辑
代码语言:javascript复制@Component
public class ReSubmitLock {
/**
* 进行一些操作
*/
@Autowired
private CuratorFramework client;
/**
* 防重复判断
* @param userId
* @param token
* @return
* @throws Exception
*/
public boolean check(String userId, String token) throws Exception {
boolean status = false;
String mainKey="/" userId;
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, mainKey);
InterProcessLock interProcessLock = interProcessReadWriteLock.writeLock();
// 获取锁
try {
interProcessLock.acquire();
// 读取数据时读取节点的属性
Stat stat = new Stat();
byte[] zkToken = client.getData()
.storingStatIn(stat)
.forPath(mainKey);
String oldToken = new String(zkToken);
System.out.println("允许的Token:" oldToken);
if (token.equals(oldToken)) {
System.out.println("校验成功!");
generateToken(userId);
status = true;
}
} catch (Exception e){
e.printStackTrace();
System.out.println("节点加锁产生错误");
}finally {
// 释放锁
interProcessLock.release();
System.out.println("释放锁!");
}
if (!status) {
System.out.println("不能重复提交表单");
}
return status;
}
/**
* 创建token
*/
public String generateToken(String userId) throws Exception {
String mainKey="/" userId;
String newToken = UUID.randomUUID().toString();
System.out.println("设置的新token为:" newToken);
client.setData()
.forPath(mainKey, newToken.getBytes());
return newToken;
}
}
测试代码
代码语言:javascript复制
@RestController
@RequestMapping(value = "/zklock")
public class TestLock {
/**
* 防重复提交
*/
@Autowired
private ReSubmitLock reSubmitLock;
/**
* 加锁
* @param userId
* @param token
* @return
* @throws Exception
*/
@GetMapping(value = "/ReSubmitLock/{userId}/{token}")
public ResponseResult test1(@PathVariable String userId,@PathVariable String token) throws Exception {
boolean f=reSubmitLock.check(userId,token);
if (f){
System.out.println("提交成功!");
return ResponseResult.success("ok");
}else{
System.out.println("重复的表单");
String newToken=reSubmitLock.generateToken(userId);
return ResponseResult.error(newToken);
}
}
}
在浏览器中发送请求:
一些反思:通过逻辑分析,这里做的防重复提交的工具适合做单个有序接口,对一批不同接口却使用相同的token进行提交的表单就会失效。所以这块的解决办法要不就是对我们的userId作为key进行改造,使之与接口向关联,进行横向扩展。要么就对value进行改造,但是对value的改造势必很难兼容我们的读写锁。除此之外如果我们要做一个公用的放重复提交的starter,那么比较理想的还是做成注解的方式,采用aop进行代理。除此之外我们在向前端传递token的时候要对业务侵入最少,因此我们可以将token设置在response的header中,这块采用threadLocal和拦截器进行处理。