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

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

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);
}
}

看上去好像没有什么问题,保存点赞之前已经检查过是否点赞了,理论上同一个人不会对同一篇文章重复点赞。但实际不是这样的。因为网络请求不是排队进来的,而是一窝蜂涌进来的。
某些时候,用户网络不好,可能很短的时间内点击了多次,由于网络传输问题,这些请求可能会同时来到我们的服务器。

  • 第一个请求 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);
}
}

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);
}

利用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);
}

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

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

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

  • 版本号控制
    CAS 算法: CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。这个比较繁杂,有兴趣的同学可以去看看。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,576评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,515评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,017评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,626评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,625评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,255评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,825评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,729评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,271评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,363评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,498评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,183评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,867评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,338评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,458评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,906评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,507评论 2 359

推荐阅读更多精彩内容