文档章节

高并发核心技术 - 幂等性 与 分布式锁

中关村的老男孩
 中关村的老男孩
发布于 06/04 20:46
字数 1601
阅读 57
收藏 6

高并发核心技术之 - 幂等性

1. 什么是幂等性

幂等性就是指:一个幂等操作任其执行多次所产生的影响均与一次执行的影响相同。 用数学的概念表达是这样的: f(f(x)) = f(x). 就像 nx1 = n 一样, x1 就是一个幂等操作。无论是乘以多少次结果都一样。

2. 常见的幂等性问题

幂等性问题经常会是由网络问题引起的,还有重复操作引起的。

场景一:比如点赞功能,一个用户只能对同一片文章点赞一次,重复点赞提示已经点过赞了。

示例代码:

    public void like(Article article,User user) {
	//检查是否点过赞
    if (checkIsLike(article,user)) {
	//点过赞了
    throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存点赞
    saveLike(article,user);
}
}
</pre>

看上去好像没有什么问题,保存点赞之前已经检查过是否点赞了,理论上同一个人不会对同一篇文章重复点赞。但实际不是这样的。因为网络请求不是排队进来的,而是一窝蜂涌进来的。

某些时候,用户网络不好,可能很短的时间内点击了多次,由于网络传输问题,这些请求可能会同时来到我们的服务器。

  • 第一个请求 checkIsLike() 返回 false , 正在执行 saveLike() 操作,还没来的及提交事务
  • 第二个请求过来了 ,checkIsLike() 返回 也是 false , 并去 执行了 saveLike() 操作

这样子,就造成了一个用户同时对一篇文章进行了多次点赞操作。

这就是典型的幂等性问题, 操作了一次和操作了两次结果不一样,因为你多点了一次赞,按照幂等性原则 不管你点击了多少次结果都一样,只点了一次赞。

很多场景都是这样造成的,比如用户重复下单,重复评论,重复提交表单等。

那怎么解决呢? 假设网络的请求是排队进来的就不会出现这个问题了。

于是我们可以改成这样:

public synchronized void like(Article article,User user) {
	//检查是否点过赞
if (checkIsLike(article,user)) {
	//点过赞了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存点赞
saveLike(article,user);
}
}
</pre>

synchronized 同步锁 这样我们的请求就会乖乖的排队进来了。

PS :这样做是效率比较低的做法,不建议这么做,只是举例子,synchronized 也不适合分布式集群场景。

场景二 : 第三方回调

我们系统经常需要和第三方系统打交道,比如微信充值,支付宝充值什么的,微信和支付宝常常会以回调你的接口通知你支付结果。为了保证你能收到回调,往往可能会回调多次。

有时候我们也为了保证数据的准确性会有个定时器去查询支付结果未知的流水,并执行响应的处理。 如果定时器的轮训和回调刚好是在同时进行,这可能又出BUG了,又进行了两次重复操作。

那么问题来了: 假设我是一个充值操作, 回调回来的时候 ,会做业务处理,成功了给用户账户加钱。这是后就要保证幂等性了, 假设微信同一笔交易给你回调了两次,如果你给用户充值了两次,这显然不合理(我是老板肯定扣你工资),所以要保证 不管微信回调你多少次 ,同一笔交易你只能给用户充一次钱。这就幂等性。

解决幂等性问题方案

  • synchronized 适合单机应用,不追求性能 ,不追求并发。
  • 分布式锁 但是往往我们的应用是分布式的集群,并且很讲究性能,并发,所以我们需要用到 分布式锁 来解决这个问题。

Redis 分布式锁:

/**
* setNx
*
*  @param key
*  @param value
*  @return
*/
public Boolean setNx(String key,Object value) {
	return redisTemplate.opsForValue().setIfAbsent(key,value);
}
/**
*  @param key 锁
*  @param waitTime 等待时间  毫秒
*  @param expireTime 超时时间  毫秒
*  @return
*/
public Boolean lock(String key,Long waitTime,Long expireTime) {
	String vlaue =  UUIDUtil.mongoObjectId();
	Boolean flag = setNx(key,vlaue);
	//尝试获取锁  成功返回
if (flag) {
	redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
	return flag;
}
else {
	//失败
//现在时间
long newTime =  System.currentTimeMillis();
	//等待过期时间
long loseTime = newTime + waitTime;
	//不断尝试获取锁成功返回
while (System.currentTimeMillis()  < loseTime) {
	Boolean testFlag = setNx(key,vlaue);
	if (testFlag) {
	redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
	return testFlag;
}
//休眠100毫秒
try {
	Thread.sleep(100);
}
catch (InterruptedException e) {
	e.printStackTrace();
}
}}return false;}/**
*  @param key
*  @return
*/
public Boolean lock(String key) {
	return lock(key,1000L,60  *  1000L);
}
/**
*  @param key
*/
public void unLock(String key) {
	remove(key);
}
</pre>

利用Redis 分布式锁 我们的代码可以改成这样:

public void like(Article article,User user) {
	String key =  "key:like"  + article.getId()  +  ":"  + user.getUserId();
	//  等待锁的时间  0  ,  过期时间  一分钟防止死锁
boolean flag = redisService.lock(key,0,60  *  1000L);
	if(!flag) {
	//获取锁失败  说明前面的请求已经获取了锁
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
//检查是否点过赞
if (checkIsLike(article,user)) {
	//点过赞了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
	//保存点赞
saveLike(article,user);
}
//删除锁
redisService.unLock(key);
}
</pre>

key 的设计也很讲究: 数据不冲突的两个业务场景,key不能冲突,不同人的key也不一样,不同的文章Key也不一样。 根据场景业务设定。

一个原则: 尽可能的缩小key的范围。 这样才能增强我们的并发。

首先我们先获取锁,获取锁成功 执行完操作,保存数据 ,删除锁。获取不到锁返回失败。设置过期时间是为了防止‘死锁’,比如机器获取到了 锁,没有设置过期时间,但是他死机了,没有删除释放锁。

  • 版本号控制 CAS 算法: CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。这个比较繁杂,有兴趣的同学可以去看看。

© 著作权归作者所有

中关村的老男孩
粉丝 40
博文 57
码字总数 132451
作品 0
海淀
架构师
私信 提问
高并发的核心技术-幂等的实现方案

一、背景 我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。 例如: 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。 我们发起一笔付...

vshcxl
2018/07/07
458
0
服务高可用:幂等性设计

QQ用得起来越少了,现在就加入300+技术微信群,公众号回复"微信群"即可加入。 什么是幂等性? 一般在服务调用时,读服务如果调用失败了,会自动按配置次数转移到别的服务上去请求。而写服务就...

架构之路
2017/12/07
0
0
支付宝防并发方案之"一锁二判三更新"

每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力。这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢? 诚然,为了实现在...

jackjoe
2017/09/16
0
0
云原生时代|分布式系统设计知识图谱(内含 22 个知识点)

我们身处于一个充斥着分布式系统解决方案的计算机时代,无论是支付宝、微信这样顶级流量产品、还是区块链、IOT等热门概念、抑或如火如荼的容器生态技术如Kubernetes,其背后的技术架构核心都...

阿里巴巴云原生
09/26
0
0
民生银行核心分布式改造实践分享

摘要:在没有分布式技术之前,国内银行的核心系统面临着很多挑战。以民生银行为例,2013年的时候每天交易量约1800万笔,整个项目的硬件和运维投入达到1.1亿多,成本非常高昂。中国民生银行总...

黄小凡
2018/06/25
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring使用ThreadPoolTaskExecutor自定义线程池及实现异步调用

多线程一直是工作或面试过程中的高频知识点,今天给大家分享一下使用 ThreadPoolTaskExecutor 来自定义线程池和实现异步调用多线程。 一、ThreadPoolTaskExecutor 本文采用 Executors 的工厂...

CREATE_17
今天
5
0
CSS盒子模型

CSS盒子模型 组成: content --> padding --> border --> margin 像现实生活中的快递: 物品 --> 填充物 --> 包装盒 --> 盒子与盒子之间的间距 content :width、height组成的 内容区域 padd......

studywin
今天
7
0
修复Win10下开始菜单、设置等系统软件无法打开的问题

因为各种各样的原因导致系统文件丢失、损坏、被修改,而造成win10的开始菜单、设置等系统软件无法打开的情况,可以尝试如下方法解决 此方法只在部分情况下有效,但值得一试 用Windows键+R打开...

locbytes
昨天
8
0
jquery 添加和删除节点

本文转载于:专业的前端网站➺jquery 添加和删除节点 // 增加一个三和一节点function addPanel() { // var newPanel = $('.my-panel').clone(true) var newPanel = $(".triple-panel-con......

前端老手
昨天
8
0
一、Django基础

一、web框架分类和wsgiref模块使用介绍 web框架的本质 socket服务端 与 浏览器的通信 socket服务端功能划分: 负责与浏览器收发消息(socket通信) --> wsgiref/uWsgi/gunicorn... 根据用户访问...

ZeroBit
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部