Java并发编程service层处理并发事务加锁可能会无效问题

 更新时间:2023年07月27日 09:43:58   作者:烟雨楼台笑江湖  
这篇文章主要介绍了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;

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Graphics2D 写图片中文乱码问题及解决

    Graphics2D 写图片中文乱码问题及解决

    这篇文章主要介绍了Graphics2D 写图片中文乱码问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java打乱数组元素简单代码例子

    Java打乱数组元素简单代码例子

    在Java编程中,我们经常需要对数组进行乱序操作(即将数组中的元素随机打乱顺序),这篇文章主要给大家介绍了关于Java打乱数组元素的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-03-03
  • java集合超详细(最新推荐)

    java集合超详细(最新推荐)

    在内存中申请一块空间用来存储数据,在Java中集合就是替换掉定长的数组的一种引用数据类型,本文介绍java集合超详细讲解,感兴趣的朋友一起看看吧
    2024-12-12
  • SpringBoot集成IJPay实现微信v3支付的示例代码

    SpringBoot集成IJPay实现微信v3支付的示例代码

    本文主要介绍了SpringBoot集成IJPay实现微信v3支付的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Java程序与C语言的区别浅析

    Java程序与C语言的区别浅析

    Java和C语言虽有相同性,但两者也有一定的不同。Java程序是面向对象的一种简单、分布式 、解释、健壮、安全、结构中立、可移植、高效能、多线程、动态的语言它是面向对象而C语言是面向过程的,这是最大的不同,对于学过C语言的我们来说,Java可以说是比较简单的编程语言
    2017-04-04
  • springboot集成Deepseek4j的项目实践

    springboot集成Deepseek4j的项目实践

    本文主要介绍了springboot集成Deepseek4j的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-03-03
  • 详解如何在SpringBoot中使用WebMvc

    详解如何在SpringBoot中使用WebMvc

    Spring Boot 是一个快速、简单的开发框架,在 Spring Boot 中,我们可以使用 WebMvc 来构建 Web 应用程序,所以本文就来讲讲如何在SpringBoot中使用WebMvc吧
    2023-06-06
  • SpringCloud基于RestTemplate微服务项目案例解析

    SpringCloud基于RestTemplate微服务项目案例解析

    这篇文章主要介绍了SpringCloud基于RestTemplate微服务项目案例,在写SpringCloud搭建微服务之前,先搭建一个不通过springcloud只通过SpringBoot和Mybatis进行模块之间通讯,通过一个案例给大家详细说明,需要的朋友可以参考下
    2022-05-05
  • java servlet手机app访问接口(二)短信验证

    java servlet手机app访问接口(二)短信验证

    这篇文章主要介绍了java servlet手机app访问接口(二),短信验证,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • jenkins如何部署应用到多个环境

    jenkins如何部署应用到多个环境

    本文介绍了如何基于流水线的方式将应用程序部署到多个环境,包括测试环境和生产环境,通过创建项目、设置参数、配置流水线、设置环境变量、配置Maven工具、构建阶段、部署测试环境和生产环境、以及清理阶段,实现了自动化部署流程
    2024-11-11

最新评论