最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊解決分析

 更新時間:2023年11月20日 11:45:12   作者:wayn  
這篇文章主要為大家介紹了java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

一 復(fù)現(xiàn)過程

文記錄博主線上項目一次用戶重復(fù)注冊問題的分析過程與解決方案

線上客戶端用戶使用微信掃碼登陸時需要再綁定一個手機號,在綁定手機后,用戶購買客戶端商品下線再登錄,發(fā)現(xiàn)用戶賬號ID被變更,已經(jīng)不是用戶剛綁定手機號時自動登錄的用戶賬號ID,查詢線上數(shù)據(jù)庫,發(fā)現(xiàn)同一個手機生成了多個賬號id,至此問題復(fù)現(xiàn)

二 分析過程

發(fā)現(xiàn)數(shù)據(jù)庫中一個手機號生成了多個用戶賬號,第一反應(yīng)是用戶在綁定手機號過程中,多次點擊綁定按鈕,導(dǎo)致綁定接口被調(diào)用多次,造成多線程并發(fā)調(diào)用用戶注冊接口,進而生成多個賬號。為了驗證我們的猜想,直接查看綁定手機后的用戶注冊方法

/**
 * 根據(jù)用戶手機號進行注冊操作
 */
// 啟動@Transactional事務(wù)注解
@Transactional(rollbackFor = Exception.class)
public boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp<BaseRespHeader, LoginRespBody> resp) {
    RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);
    boolean lock;
    try {
        lock = redisLock.lock();
        // 使用redis分布式鎖
        if (lock) {
            // 查詢數(shù)據(jù)庫該用戶手機號是否插入成功,已存在則退出操作
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects.nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // 執(zhí)行用戶注冊操作,包含插入用戶表、訂單表、是否被邀請
            ...
        }
    } catch (Exception e) {
        log.error("用戶注冊失敗:", e);
        throw new Exception("用戶注冊失敗");
    } finally {
        redisLock.unLock();
    }
    // 添加注冊日志,上報到數(shù)據(jù)分析平臺...
    return true;
}

初看代碼,在分布式環(huán)境中,先加分布式鎖保證同時只能被一個線程執(zhí)行,然后判斷數(shù)據(jù)庫中是否存在用戶手機信息,已存在則退出,不存在則執(zhí)行用戶注冊操作,咋以為邏輯上沒有問題,但是線上環(huán)境確實就是出現(xiàn)了相同手機號重復(fù)注冊的問題,首先代碼被 @Transactional 注解包含,就是在自動事務(wù)中執(zhí)行注冊邏輯

現(xiàn)在博主帶大家回憶一下,MySQL 事務(wù)的隔離級別有4個

  • Read uncommitted:讀取未提交,其他事務(wù)只要修改了數(shù)據(jù),即使未提交,本事務(wù)也能看到修改后的數(shù)據(jù)值。
  • Read committed:讀取已提交,其他事務(wù)提交了對數(shù)據(jù)的修改后,本事務(wù)就能讀取到修改后的數(shù)據(jù)值。
  • Repeatable read:可重復(fù)讀,無論其他事務(wù)是否修改并提交了數(shù)據(jù),在這個事務(wù)中看到的數(shù)據(jù)值始終不受其他事務(wù)影響。
  • Serializable:串行化,一個事務(wù)一個事務(wù)的執(zhí)行。
  • MySQL數(shù)據(jù)庫默認(rèn)使用可重復(fù)讀( Repeatable read)。

隔離級別越高,越能保證數(shù)據(jù)的完整性和一致性,但是對并發(fā)性能的影響也越大,MySQL的默認(rèn)隔離級別是讀可重復(fù)讀。在上述場景里,也就是說,無論其他線程事務(wù)是否提交了數(shù)據(jù),當(dāng)前線程所在事務(wù)中看到的數(shù)據(jù)值始終不受其他事務(wù)影響

說人話(劃重點):就是在 MySQL 中一個線程所在事務(wù)是讀不到另一個線程事務(wù)未提交的數(shù)據(jù)的

下面結(jié)合上述代碼給出分析過程:上述注冊邏輯都包含在 Spring 提供的自動事務(wù)中,整個方法都在事務(wù)中。而加鎖也在事務(wù)中執(zhí)行。最終導(dǎo)致我們注冊 線程B 在當(dāng)前事物中查詢不到另一個注冊 線程A 所在事物未提交的數(shù)據(jù), 舉個例子

eg:

  • 當(dāng)用戶執(zhí)行注冊操作,重復(fù)點擊注冊按鈕時,假設(shè)線程A和B同時執(zhí)行到 redisLock.lock()時,假設(shè)線程A獲取到鎖,線程B進入自旋等待,線程A執(zhí)行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,發(fā)現(xiàn)用戶手機不存在數(shù)據(jù)庫中,進行注冊操作(添加用戶信息入庫等),執(zhí)行完畢,釋放鎖。執(zhí)行后續(xù)添加注冊日志,上報到數(shù)據(jù)分析平臺操作,注意此時事務(wù)還未提交。
  • 線程B終于獲取到鎖,執(zhí)行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,在我們一開始的假設(shè)中,以為這里會返回用戶已存在,但是實際執(zhí)行結(jié)果并不是這樣的。原因就是線程A的事務(wù)還未提交,線程B讀不到線程A未提交事務(wù)的數(shù)據(jù)也就是說查不到用戶已注冊信息,至此,我們知道了用戶重復(fù)注冊的原因。

三 解決方案

給出三種解決方案

3.1 修改事務(wù)范圍

將事務(wù)的操作代碼最小化,保證在加鎖結(jié)束前完成事務(wù)提交,代碼如下開啟手動事務(wù),這樣其他線程在加鎖代碼塊中就能看到最新數(shù)據(jù)

@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
private boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp<BaseRespHeader, LoginRespBody> resp) {
    RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);
    boolean lock;
    TransactionStatus transaction = null;
    try {
        lock = redisLock.lock();
        // 使用redis分布式鎖
        if (lock) {
            // 查詢數(shù)據(jù)庫該用戶手機號是否插入成功,已存在則退出操作
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects.nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // 手動開啟事務(wù)
            transaction = platformTransactionManager.getTransaction(transactionDefinition);
            // 執(zhí)行用戶注冊操作,包含插入用戶表、訂單表、是否被邀請
            ...
            // 手動提交事務(wù)
            platformTransactionManager.commit(transaction);
            ...
        }
    } catch (Exception e) {
        log.error("用戶注冊失?。?, e);
        if (transaction != null) {
            platformTransactionManager.rollback(transaction);
        }
        return false;
    } finally {
        redisLock.unLock();
    }
    // 添加注冊日志,上報到數(shù)據(jù)分析平臺...
    return true;
}

3.2 在用戶注冊時針對注冊接口添加防重復(fù)提交處理

下面給出一個基于 AOP 切面 + 注解實現(xiàn)的限流邏輯

/**
 * 限流枚舉
 */
public enum LimitType {
    // 默認(rèn)
    CUSTOMER,
    //  by ip addr
    IP
}
/**
 * 自定義接口限流
 *
 * @author jacky
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
    boolean useAccount() default true;
    String name() default "";
    String key() default "";
    String prefix() default "";
    int period();
    int count();
    LimitType limitType() default LimitType.CUSTOMER;
}
/**
 * 限制器切面
 */
@Slf4j
@Aspect
@Component
public class LimitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Pointcut("@annotation(com.dogame.dragon.sparrow.framework.common.annotation.Limit)")
    public void pointcut() {
    }
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attrs.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        Limit limit = signatureMethod.getAnnotation(Limit.class);
        boolean useAccount = limit.useAccount();
        LimitType limitType = limit.limitType();
        String key = limit.key();
        if (StringUtils.isEmpty(key)) {
            if (limitType == LimitType.IP) {
                key = IpUtils.getIpAddress(request);
            } else {
                key = signatureMethod.getName();
            }
        }
        if (useAccount) {
            LoginMember loginMember = LocalContext.getLoginMember();
            if (loginMember != null) {
                key = key + "_" + loginMember.getAccount();
            }
        }
        String join = StringUtils.join(limit.prefix(), key, "_", request.getRequestURI().replaceAll("/", "_"));
        List<String> strings = Collections.singletonList(join);
        String luaScript = buildLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long count = stringRedisTemplate.execute(redisScript, strings, limit.count() + "", limit.period() + "");
        if (null != count && count.intValue() <= limit.count()) {
            log.info("第{}次訪問key為 {},描述為 [{}] 的接口", count, strings, limit.name());
            return joinPoint.proceed();
        } else {
            throw new DragonSparrowException("短時間內(nèi)訪問次數(shù)受限制");
        }
    }
    /**
     * 限流腳本
     */
    private String buildLuaScript() {
        return "local c" +
                "\nc = redis.call('get',KEYS[1])" +
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                "\nc = redis.call('incr',KEYS[1])" +
                "\nif tonumber(c) == 1 then" +
                "\nredis.call('expire',KEYS[1],ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }
}
  • 前端針對綁定手機按鈕添加防止連點處理

四 總結(jié)

線上項目對于 Spring 提供的自動事務(wù)注解使用要多加思考,盡可能減少事務(wù)影響范圍,針對注冊等按鈕要在前后端添加防重復(fù)點擊處理

以上就是java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊解決分析的詳細(xì)內(nèi)容,更多關(guān)于java多線程事務(wù)重復(fù)注冊的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java?jar打包成exe應(yīng)用程序的詳細(xì)步驟

    Java?jar打包成exe應(yīng)用程序的詳細(xì)步驟

    本文主要介紹了Java?jar打包成exe應(yīng)用程序的詳細(xì)步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • java利用Ant腳本生成war包全過程

    java利用Ant腳本生成war包全過程

    這篇文章主要為大家詳細(xì)介紹了java利用Ant腳本生成war包全過程,感興趣的朋友可以參考一下
    2016-03-03
  • Java軟件生產(chǎn)監(jiān)控工具Btrace使用方法詳解

    Java軟件生產(chǎn)監(jiān)控工具Btrace使用方法詳解

    這篇文章主要介紹了Java軟件生產(chǎn)監(jiān)控工具Btrace使用方法詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-07-07
  • Java開源工具iText生成PDF簡單實例

    Java開源工具iText生成PDF簡單實例

    這篇文章主要介紹了Java開源工具iText生成PDF簡單實例,本文給出了3段代碼實例,講解創(chuàng)建一個簡單PDF文件,在PDF中添加表格以及在PDF中添加圖片,需要的朋友可以參考下
    2015-07-07
  • Java 深入探討設(shè)計模式之原型模式篇

    Java 深入探討設(shè)計模式之原型模式篇

    設(shè)計模式(Design pattern)是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計經(jīng)驗的總結(jié)。使用設(shè)計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性
    2021-10-10
  • java使用DelayQueue實現(xiàn)延時任務(wù)

    java使用DelayQueue實現(xiàn)延時任務(wù)

    項目中經(jīng)常會用到類似一些需要延遲執(zhí)行的功能,比如緩存,java提供了DelayQueue來很輕松的實現(xiàn)這種功能,下面小編就來和大家介紹一下如何使用DelayQueue實現(xiàn)延時任務(wù)吧
    2023-10-10
  • Gradle環(huán)境下導(dǎo)出Swagger為PDF的步驟詳解

    Gradle環(huán)境下導(dǎo)出Swagger為PDF的步驟詳解

    這篇文章主要介紹了Gradle環(huán)境下導(dǎo)出Swagger為PDF的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-06-06
  • maven中deploy命令報401錯誤的原因及解決方案

    maven中deploy命令報401錯誤的原因及解決方案

    在mac版idea使用過程中有時候會出現(xiàn)deploy時候報401錯誤,所以本文給大家介紹了maven 中deploy命令報401錯誤的原因及解決方案,文章通過圖文結(jié)合的方式講解的非常詳細(xì),需要的朋友可以參考下
    2024-05-05
  • Java觀察者設(shè)計模式(Observable和Observer)

    Java觀察者設(shè)計模式(Observable和Observer)

    這篇文章主要介紹了 Java觀察者設(shè)計模式(Observable和Observer)的相關(guān)資料,需要的朋友可以參考下
    2015-12-12
  • Invalid?bound?statement?(not?found)出現(xiàn)原因以及解決辦法

    Invalid?bound?statement?(not?found)出現(xiàn)原因以及解決辦法

    這篇文章主要給大家介紹了關(guān)于Invalid?bound?statement?(not?found)出現(xiàn)原因以及解決辦法的相關(guān)資料,文中給出了詳細(xì)的解決方法,需要的朋友可以參考下
    2023-07-07

最新評論

都匀市| 吴忠市| 偃师市| 鄂温| 衡阳市| 吉首市| 莫力| 韩城市| 通城县| 龙南县| 化隆| 钦州市| 永川市| 调兵山市| 富阳市| 马鞍山市| 张家川| 江门市| 探索| 招远市| 江川县| 天长市| 闽侯县| 荣昌县| 宁陕县| 柯坪县| 鄂托克旗| 岑溪市| 威信县| 临猗县| 屏东市| 宣城市| 沛县| 盐源县| 武隆县| 嵊泗县| 陈巴尔虎旗| 荃湾区| 奉节县| 德兴市| 营山县|