Java并發(fā)編程service層處理并發(fā)事務(wù)加鎖可能會(huì)無(wú)效問(wèn)題
問(wèn)題描述
近期寫(xiě)了一個(gè)單體架構(gòu)秒殺的功能,在對(duì)商品庫(kù)存進(jìn)行扣減,有線程安全問(wèn)題,因此加了Lock鎖進(jìn)行同步,但發(fā)現(xiàn)加鎖后并沒(méi)有控制住庫(kù)存線程安全的問(wèn)題,導(dǎo)致庫(kù)存仍被超發(fā)。
輸出一下代碼:
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSeckillLock(long seckillId, long userId) {
/**
* 這里加鎖,還是會(huì)出現(xiàn)超賣(mài)
*
* 因?yàn)檫M(jìn)入service方法中時(shí),spring事務(wù)已經(jīng)開(kāi)啟,隔離級(jí)別默認(rèn)是可重復(fù)讀,
* 因?yàn)槭聞?wù)先開(kāi)啟,后加鎖,隔離級(jí)別為可重復(fù)讀的情況下,當(dāng)前線程讀不到其他線程更新的數(shù)據(jù),
* 所以就會(huì)出現(xiàn)超賣(mài)的情況
*
* 下面方法通過(guò)aop加鎖,order = 1,在事務(wù)開(kāi)啟之前加鎖
*
* 還有就是直接在controller中加鎖
*/
lock.lock();
try {
//校驗(yàn)庫(kù)存
String nativeSql = "SELECT number FROM seckill WHERE seckill_id = ?";
Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
Long number = ((Number) object).longValue();
System.out.println(">>>>>>>>>>>>>>>>>>>>>> number : {}" + number);
if(number > 0){
//扣庫(kù)存
nativeSql = "UPDATE seckill SET number=? WHERE seckill_id = ?";
dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{number - 1, seckillId});
//創(chuàng)建訂單
SuccessKilled killed = new SuccessKilled();
killed.setSeckillId(seckillId);
killed.setUserId(userId);
killed.setState((short)0);
killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
dynamicQuery.save(killed);
return Result.ok(SeckillStatEnum.SUCCESS);
//支付
}else{
return Result.error(SeckillStatEnum.END);
}
} finally {
lock.unlock();
}
// https://cloud.tencent.com/developer/article/1630866
// finally 在 return 之后時(shí),先執(zhí)行 finally 后,再執(zhí)行該 return;
// finally 內(nèi)含有 return 時(shí),直接執(zhí)行其 return 后結(jié)束;
// finally 在 return 前,執(zhí)行完 finally 后再執(zhí)行 return。
// return Result.ok(SeckillStatEnum.SUCCESS);
}問(wèn)題分析
由于spring事務(wù)是通過(guò)AOP實(shí)現(xiàn)的,所以在startSeckillLock()方法執(zhí)行之前會(huì)開(kāi)啟事務(wù),之后會(huì)有提交事務(wù)的邏輯。
而lock的動(dòng)作是發(fā)生在事務(wù)之內(nèi)。
數(shù)據(jù)庫(kù)默認(rèn)的事務(wù)隔離級(jí)別為可重復(fù)讀(repeatable-read)。
因?yàn)槭鞘聞?wù)先開(kāi)啟后加鎖,隔離級(jí)別為可重復(fù)讀的情況下,當(dāng)前線程是讀取不到其他線程更新的數(shù)據(jù),也就是說(shuō)其他線程雖然更新了庫(kù)存且事務(wù)也提交了,但是因?yàn)楫?dāng)前線程已經(jīng)開(kāi)啟了事務(wù)(可重復(fù)讀的隔離級(jí)別),所以當(dāng)前線程在事務(wù)中獲取到的仍然是開(kāi)啟事務(wù)時(shí)的庫(kù)存,所以就會(huì)出現(xiàn)超賣(mài)的情況。
問(wèn)題解決
一:在controller層加鎖
二:在service層自己定義事務(wù)的開(kāi)啟和提交,加鎖的代碼方到開(kāi)啟事務(wù)之前,解鎖在提交事務(wù)之后
三:AOP+鎖
自定義注解ServiceLock:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Servicelock {
String description() default "";
}自定義切面LockAspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Component
@Aspect
@Order(1)
public class LockAspect {
private static final Lock lock = new ReentrantLock();
@Pointcut("@annotation(com.wjy.seckill.common.aop.ServiceLock)")
public void lockAspect() {
}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
lock.lock();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
lock.unlock();
}
return result;
}
}切入秒殺方法:
@Override
@ServiceLock
@Transactional(rollbackFor = Exception.class)
public Result startSeckillAopLock(long seckillId, long userId) {
//校驗(yàn)庫(kù)存
String nativeSql = "SELECT number FROM seckill WHERE seckill_id = ?";
Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
Long number = ((Number) object).longValue();
System.out.println(">>>>>>>>>>>>>>>>>>>>>> number : {}" + number);
if(number > 0){
//扣庫(kù)存
nativeSql = "UPDATE seckill SET number=? WHERE seckill_id = ?";
dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{number - 1, seckillId});
//創(chuàng)建訂單
SuccessKilled killed = new SuccessKilled();
killed.setSeckillId(seckillId);
killed.setUserId(userId);
killed.setState((short)0);
killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
dynamicQuery.save(killed);
return Result.ok(SeckillStatEnum.SUCCESS);
//支付
}else{
return Result.error(SeckillStatEnum.END);
}
}至此問(wèn)題解決
表結(jié)構(gòu)
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50732 Source Host : localhost:3306 Source Schema : spring-boot-seckill Target Server Type : MySQL Target Server Version : 50732 File Encoding : 65001 Date: 05/01/2022 15:51:06 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for seckill -- ---------------------------- DROP TABLE IF EXISTS `seckill`; CREATE TABLE `seckill` ( `seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品庫(kù)存id', `name` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品名稱(chēng)', `number` int(11) NOT NULL COMMENT '庫(kù)存數(shù)量', `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺開(kāi)啟時(shí)間', `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺結(jié)束時(shí)間', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', `version` int(11) NOT NULL COMMENT '版本號(hào)', PRIMARY KEY (`seckill_id`) USING BTREE, INDEX `idx_start_time`(`start_time`) USING BTREE, INDEX `idx_end_time`(`end_time`) USING BTREE, INDEX `idx_create_time`(`create_time`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1004 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒殺庫(kù)存表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of seckill -- ---------------------------- INSERT INTO `seckill` VALUES (1000, '1000元秒殺iphone8', 100, '2018-05-10 15:31:53', '2018-05-10 15:31:53', '2018-05-10 15:31:53', 0); INSERT INTO `seckill` VALUES (1001, '500元秒殺ipad2', 100, '2018-05-10 15:31:53', '2018-05-10 15:31:53', '2018-05-10 15:31:53', 0); INSERT INTO `seckill` VALUES (1002, '300元秒殺小米4', 100, '2018-05-10 15:31:53', '2018-05-10 15:31:53', '2018-05-10 15:31:53', 0); INSERT INTO `seckill` VALUES (1003, '200元秒殺紅米note', 100, '2018-05-10 15:31:53', '2018-05-10 15:31:53', '2018-05-10 15:31:53', 0); -- ---------------------------- -- Table structure for success_killed -- ---------------------------- DROP TABLE IF EXISTS `success_killed`; CREATE TABLE `success_killed` ( `seckill_id` bigint(20) NOT NULL COMMENT '秒殺商品id', `user_id` bigint(20) NOT NULL COMMENT '用戶(hù)Id', `state` tinyint(4) NOT NULL COMMENT '狀態(tài)標(biāo)示:-1指無(wú)效,0指成功,1指已付款', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', PRIMARY KEY (`seckill_id`, `user_id`) USING BTREE, INDEX `idx_create_time`(`create_time`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒殺成功明細(xì)表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of success_killed -- ---------------------------- SET FOREIGN_KEY_CHECKS = 1;
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Graphics2D 寫(xiě)圖片中文亂碼問(wèn)題及解決
這篇文章主要介紹了Graphics2D 寫(xiě)圖片中文亂碼問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
SpringBoot集成IJPay實(shí)現(xiàn)微信v3支付的示例代碼
本文主要介紹了SpringBoot集成IJPay實(shí)現(xiàn)微信v3支付的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
springboot集成Deepseek4j的項(xiàng)目實(shí)踐
本文主要介紹了springboot集成Deepseek4j的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
SpringCloud基于RestTemplate微服務(wù)項(xiàng)目案例解析
這篇文章主要介紹了SpringCloud基于RestTemplate微服務(wù)項(xiàng)目案例,在寫(xiě)SpringCloud搭建微服務(wù)之前,先搭建一個(gè)不通過(guò)springcloud只通過(guò)SpringBoot和Mybatis進(jìn)行模塊之間通訊,通過(guò)一個(gè)案例給大家詳細(xì)說(shuō)明,需要的朋友可以參考下2022-05-05
java servlet手機(jī)app訪問(wèn)接口(二)短信驗(yàn)證
這篇文章主要介紹了java servlet手機(jī)app訪問(wèn)接口(二),短信驗(yàn)證,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
jenkins如何部署應(yīng)用到多個(gè)環(huán)境
本文介紹了如何基于流水線的方式將應(yīng)用程序部署到多個(gè)環(huán)境,包括測(cè)試環(huán)境和生產(chǎn)環(huán)境,通過(guò)創(chuàng)建項(xiàng)目、設(shè)置參數(shù)、配置流水線、設(shè)置環(huán)境變量、配置Maven工具、構(gòu)建階段、部署測(cè)試環(huán)境和生產(chǎn)環(huán)境、以及清理階段,實(shí)現(xiàn)了自動(dòng)化部署流程2024-11-11

