Java并发编程service层处理并发事务加锁可能会无效问题
作者:烟雨楼台笑江湖
这篇文章主要介绍了Java并发编程service层处理并发事务加锁可能会无效问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
问题描述
近期写了一个单体架构秒杀的功能,在对商品库存进行扣减,有线程安全问题,因此加了Lock锁进行同步,但发现加锁后并没有控制住库存线程安全的问题,导致库存仍被超发。
输出一下代码:
@Override @Transactional(rollbackFor = Exception.class) public Result startSeckillLock(long seckillId, long userId) { /** * 这里加锁,还是会出现超卖 * * 因为进入service方法中时,spring事务已经开启,隔离级别默认是可重复读, * 因为事务先开启,后加锁,隔离级别为可重复读的情况下,当前线程读不到其他线程更新的数据, * 所以就会出现超卖的情况 * * 下面方法通过aop加锁,order = 1,在事务开启之前加锁 * * 还有就是直接在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}); //创建订单 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 之后时,先执行 finally 后,再执行该 return; // finally 内含有 return 时,直接执行其 return 后结束; // finally 在 return 前,执行完 finally 后再执行 return。 // return Result.ok(SeckillStatEnum.SUCCESS); }
问题分析
由于spring事务是通过AOP实现的,所以在startSeckillLock()方法执行之前会开启事务,之后会有提交事务的逻辑。
而lock的动作是发生在事务之内。
数据库默认的事务隔离级别为可重复读(repeatable-read)。
因为是事务先开启后加锁,隔离级别为可重复读的情况下,当前线程是读取不到其他线程更新的数据,也就是说其他线程虽然更新了库存且事务也提交了,但是因为当前线程已经开启了事务(可重复读的隔离级别),所以当前线程在事务中获取到的仍然是开启事务时的库存,所以就会出现超卖的情况。
问题解决
一:在controller层加锁
二:在service层自己定义事务的开启和提交,加锁的代码方到开启事务之前,解锁在提交事务之后
三: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}); //创建订单 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); } }
至此问题解决
表结构
/* 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 '库存数量', `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀开启时间', `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀结束时间', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `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 '状态标示:-1指无效,0指成功,1指已付款', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 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 = '秒杀成功明细表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of success_killed -- ---------------------------- SET FOREIGN_KEY_CHECKS = 1;
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。