文章目录
- 前言
- 1.全局唯一ID
- 1.1 前端防止重复提交
- 1.2 token机制
- 1.3 数据库表加唯一约束
- 2.幂等下 ABA问题 与乐观锁
- 2.1 乐观锁
- 2.2 如何解决ABA问题?
- 3.分布式锁和事务
- 3.1 分布式锁:
- 3.2. 分布式事务
前言
幂等性(Idempotence)
是一个在计算机科学中使用的术语。当某个操作无论进行一次或多次都产生相同的结果,我们就说这个操作是幂等的。
例如,删除文件的操作就是幂等的,因为无论你尝试删除一次还是两次,结果都是文件被删除。相对地,计数器增加操作就不是幂等的,因为每次操作,计数器的值都会改变。
在分布式系统和网络协议中,幂等性是非常重要的特性。由于网络延迟,服务端可能会接收到同一条消息的多个副本,如果此类操作是幂等的,那么就不会出现任何问题。
解决方案:
- 全局唯一ID :为每个请求分配一个唯一的任务 ID,并确保服务器对每个任务只执行一次。如果服务器接收到相同的任务 ID,它将忽略这个任务或返回之前任务的结果。
- Token 机制: 根据业务的操作和内容生成一个Token值(全局唯一ID,执行之前判断方法是否执行过。
- 发送确认消息:客户端在发送完请求后并不立即进行下一步操作,而是等待服务器的确认消息。如果客户端没有收到确认消息,那么它会重新发送请求。
- 使用分布式锁或事务:通过这种方式,可以确保一次只有一个任务在执行,避免因重复执行同一个任务而产生的问题。
- 使用乐观锁:乐观锁对数据进行更新时,会检查数据在此期间是否被别人修改过,如果被修改过,则拒绝更新。
- 使用“compare-and-swap”等原子操作:原子操作可以在不使用锁的情况下完成复杂的更新操作,这也可以保证幂等性。
幂等可以用来解决什么问题?
幂等性可以用于解决在分布式系统中的重复请求问题。在分布式系统中,由于网络等原因,可能会导致同一个请求被多次执行,从而可能会造成脏数据或资源浪费等问题。通过实现幂等性,可以确保相同的请求只被执行一次,从而防止出现重复请求的情况。这样可以提高系统的稳定性、可靠性和性能。另外,幂等性也可以用于实现事务的原子性,确保事务只被执行一次,从而防止出现数据不一致的情况。
1.全局唯一ID
全局唯一ID的生成,需要根据实际的业务需要进行生成,单机的部署一般使用UUID或者数据库自增ID就ok,分布式下现在比较流行的雪花算法生成全局唯一ID。单机也需要考虑后期业务的扩展进行使用全局唯一ID,根据阿里规范:
单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表
1.1 前端防止重复提交
一般开发中,前端也会做一些简单防止重复提交操作,一般的操作就是提交请求后禁用操作。此处就不在赘述,比较简单,此做法不能解决问题,只能简单解决人为的操作。
解决不了由于网络波动,网络重试机制导致数据不一致问题。
1.2 token机制
Token的机制通过进行业务操作之前进行根据业务生成一个token,token可以使用雪花算法进行生成,进行业务操作之前先获取token,获取成功请求业务携带token,请求,后台会根据逻辑进行有效操作的判断。后端判断该 token 是否存在,如存在,则为第一次提交,放行并删除token,如无token,第二次提交,阻拦该请求。
高并发环境下还需要考虑操作的原子性。
此处使用redis来存生成的token。
安装和具体的操作参考:
centos7下安装redis以及本地连接注意事项
Springboot整合redis
简单token服务代码
/**
* token服务
*/
@Service
public class TokenService {
@Autowired
RedisService redisService;
/**
* 获取token
* @param bizType
* @return
*/
public String getToken(String bizType){
///业务的key
String key = bizType+":"+UUID.randomUUID().toString();
redisService.saveKeyValue(key,1,10);
return key;
}
/**
* 校验token
* @param token
* @return
*/
public boolean verifyToken(String token){
///token
String value = redisService.getValueByKey(token);
//判断值是否存在
if(!StringUtils.isEmpty(value)){
//存在删除
redisService.delete(token);
return true;
}
return false;
}
controller:
@RestController
public class TokenController {
@Autowired
TokenService tokenService;
@GetMapping("/getToken/{bizType}")
public String getToken(@PathVariable("bizType") String bizType){
return tokenService.getToken(bizType);
}
}
1.3 数据库表加唯一约束
比如用户表加上身份证号作为唯一的约束,也可以防止重复提交用户注册。对于一些业务唯一不明确的也能导致不幂等性。
2.幂等下 ABA问题 与乐观锁
ABA问题
:是在并发环境中经常会遇到的问题,最典型的例子就是CAS(Compare And Swap)操作。CAS操作是一种乐观加锁机制,它执行时首先把某个内存值A读入到未提交缓冤区,在此期间可能其他线程也修改了这个内存场所的值变成了B,若这个时候重来执行CAS操作,尽管它能检测到这个内存值已经被修改过,但修改过后的这个值B又被其他线程改变成了A,这样CAS操作就可能错误地成功。这就是ABA问题。
2.1 乐观锁
乐观锁一种并发控制技术。在写入数据时,并不加锁,而是假定没有其他线程与其同时修改数据。当操作系统执行写入操作时,它会检查没有线程已经修改从开头到现在的数据。如果检测到其他线程已经修改了该数据,那么操作系统就会拒绝写入,并通知失败。需要重试直到没有其他线程修改需要写入的数据。
乐观锁主要解决了悲观锁长期占据锁而导致其他线程长时间等待锁的问题。
2.2 如何解决ABA问题?
- 使用版本号:在每个变量后面追加上版本号,每次修改该变量都对版本号加一,这就能解决ABA问题了。
- 使用原子类:Java从1.5开始提供了一个原子包(java.util.concurrent.atomic),在这个包中提供了一些原子类,其中ABA问题可以使用类AtomicStampedReference进行解决,它通过控制变量的版本解决了ABA问题。
3.分布式锁和事务
3.1 分布式锁:
其主要目的是在系统内提供一种机制,来确保在任何时刻只有一个节点可以执行某个程序区块,所以分布式锁主要目的是为了防止并发操作引发的数据不一致问题。
分布式锁通过一种协议来决定哪一个请求可以获得锁,以执行相应的操作。例如,一个典型的应用程序可能需要对数据库中的一个数据进行更新,程序在进行更新操作的时候会先尝试获取分布式锁,如果获取成功则进行操作,如果获取失败则等待或者抛出错误。
3.2. 分布式事务
事务的核心是:ACID原则(原子性、一致性、隔离性、持久性)。
在一个分布式系统中,为了保证跨多个节点的数据一致性和操作可靠性,通常需要引入一种机制——分布式事务。该机制可以确保分布式系统中多个操作要么全都成功执行,要么全都不执行(如果某处失败,其他所有成功的操作也需要回滚)。
分布式锁主要用于保证在多节点环境下,某一时刻只有一个节点能够操作某资源,用于解决并发问题;而分布式事务,主要用于确保在多节点环境下,一个业务流程中包含的多个操作要么全部成功,要么全部失败,从而保证数据的一致性。