Java并發(fā)編程service層處理并發(fā)事務(wù)加鎖可能會無效問題
問題描述
近期寫了一個單體架構(gòu)秒殺的功能,在對商品庫存進(jìn)行扣減,有線程安全問題,因此加了Lock鎖進(jìn)行同步,但發(fā)現(xiàn)加鎖后并沒有控制住庫存線程安全的問題,導(dǎo)致庫存仍被超發(fā)。
輸出一下代碼:
@Override @Transactional(rollbackFor = Exception.class) public Result startSeckillLock(long seckillId, long userId) { /** * 這里加鎖,還是會出現(xiàn)超賣 * * 因為進(jìn)入service方法中時,spring事務(wù)已經(jīng)開啟,隔離級別默認(rèn)是可重復(fù)讀, * 因為事務(wù)先開啟,后加鎖,隔離級別為可重復(fù)讀的情況下,當(dāng)前線程讀不到其他線程更新的數(shù)據(jù), * 所以就會出現(xiàn)超賣的情況 * * 下面方法通過aop加鎖,order = 1,在事務(wù)開啟之前加鎖 * * 還有就是直接在controller中加鎖 */ lock.lock(); try { //校驗庫存 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){ //扣庫存 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 之后時,先執(zhí)行 finally 后,再執(zhí)行該 return; // finally 內(nèi)含有 return 時,直接執(zhí)行其 return 后結(jié)束; // finally 在 return 前,執(zhí)行完 finally 后再執(zhí)行 return。 // return Result.ok(SeckillStatEnum.SUCCESS); }
問題分析
由于spring事務(wù)是通過AOP實現(xiàn)的,所以在startSeckillLock()方法執(zhí)行之前會開啟事務(wù),之后會有提交事務(wù)的邏輯。
而lock的動作是發(fā)生在事務(wù)之內(nèi)。
數(shù)據(jù)庫默認(rèn)的事務(wù)隔離級別為可重復(fù)讀(repeatable-read)。
因為是事務(wù)先開啟后加鎖,隔離級別為可重復(fù)讀的情況下,當(dāng)前線程是讀取不到其他線程更新的數(shù)據(jù),也就是說其他線程雖然更新了庫存且事務(wù)也提交了,但是因為當(dāng)前線程已經(jīng)開啟了事務(wù)(可重復(fù)讀的隔離級別),所以當(dāng)前線程在事務(wù)中獲取到的仍然是開啟事務(wù)時的庫存,所以就會出現(xiàn)超賣的情況。
問題解決
一:在controller層加鎖
二:在service層自己定義事務(wù)的開啟和提交,加鎖的代碼方到開啟事務(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) { //校驗庫存 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){ //扣庫存 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); } }
至此問題解決
表結(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 '商品庫存id', `name` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品名稱', `number` int(11) NOT NULL COMMENT '庫存數(shù)量', `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺開啟時間', `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺結(jié)束時間', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間', `version` int(11) NOT NULL COMMENT '版本號', 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 = '秒殺庫存表' 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 '用戶Id', `state` tinyint(4) NOT NULL COMMENT '狀態(tài)標(biāo)示:-1指無效,0指成功,1指已付款', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間', 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é)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot集成IJPay實現(xiàn)微信v3支付的示例代碼
本文主要介紹了SpringBoot集成IJPay實現(xiàn)微信v3支付的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07SpringCloud基于RestTemplate微服務(wù)項目案例解析
這篇文章主要介紹了SpringCloud基于RestTemplate微服務(wù)項目案例,在寫SpringCloud搭建微服務(wù)之前,先搭建一個不通過springcloud只通過SpringBoot和Mybatis進(jìn)行模塊之間通訊,通過一個案例給大家詳細(xì)說明,需要的朋友可以參考下2022-05-05java servlet手機(jī)app訪問接口(二)短信驗證
這篇文章主要介紹了java servlet手機(jī)app訪問接口(二),短信驗證,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12jenkins如何部署應(yīng)用到多個環(huán)境
本文介紹了如何基于流水線的方式將應(yīng)用程序部署到多個環(huán)境,包括測試環(huán)境和生產(chǎn)環(huán)境,通過創(chuàng)建項目、設(shè)置參數(shù)、配置流水線、設(shè)置環(huán)境變量、配置Maven工具、構(gòu)建階段、部署測試環(huán)境和生產(chǎn)環(huán)境、以及清理階段,實現(xiàn)了自動化部署流程2024-11-11