分布式锁+@Transactional导致事物失效???

起因:

前两天看到一段代码,分布式锁于@Transactional组合使用导致接口最终结果有误,趁今天周末有空我们写个简单的demo来复现一下,这不是该放中秋节抢票了吗,那咱就来模拟下抢票呗。

controller层代码

import com.syl.cloud_2024.service.TransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class TransactionalController {

    @Autowired
    private TransactionalService transactionalService;

    @GetMapping("testA")
    public void testA() {
        // 模拟50个人买票
        for (int i = 1; i <= 50; i++){
            new Thread() {
                @Override
                public void run() {
                        transactionalService.test();
                }
            }.start();
        }
    }
}

service层代码

public interface TransactionalService {

    /**
     *
     * @author yeYingXuan
     */
    void test();
}

service实现层代码

@Service
@Slf4j
public class TransactionalServiceImpl implements TransactionalService {

    @Autowired
    FarmService farmService;
    @Autowired
    RedisLockService redisLockService;
    @Autowired
    private TestAMapper testAMapper;



    @Override
    @Transactional(rollbackFor = Exception.class)
    public void test() {
        String lockKey = "lock:Api:test2";
        try {
            // 加锁并设置过期时间3秒,等待时常3秒
            boolean locked = redisLockService.tryLock(lockKey, Thread.currentThread().getName(), 3, TimeUnit.SECONDS, 3000);
            if (locked) {
                TestA oldTestA = testAMapper.selectById(1);
                int level = oldTestA.getLevel() - 1;
                log.info("我是线程{},我买完还剩余{}张高铁票", Thread.currentThread().getName(), level);
                TestA testA = new TestA();
                testA.setId(1);
                testA.setLevel(level);
                testAMapper.updateById(testA);
            } else {
                throw new RuntimeException("稍后再试");
            }
        } finally {
            // 释放锁(只有当前线程持有锁时才释放锁)
            redisLockService.unlock(lockKey, Thread.currentThread().getName());
        }
    }
}

准备数据,一共50张票,50个人买完看看还剩余多少。

结果明显不对,50个人,50张票,卖完了怎么还剩余2张票呢?我们再来看看买票日志

看到结果后明显有2张票卖重复了,脑补下画面(这4个人上车后扭打一起。。。)

到此处我们已经复现出来了问题,接着就是分析一下问题发生原因,以及怎么解决。

分析原因:

乍看一下代码貌似没问题,有redis锁控制并发,有事务控制数量,事务隔离级别为读已提交,保证查到的剩余数量一定是上一个人买成功后的数量。但是问题就出现在是因为在事务内加的锁与释放锁。

我们都知道,@Transactional 是 Spring 框架中用于声明式事务管理的一个注解。它的实现机制主要基于 AOP(面向切面编程)和动态代理。而进入到test()前会代理类会开启事务,test()方法走完后会走代理类会走提交事务方法。所以问题原因就是test()执行完,锁已经释放了,而事务还没提交。接下来我们验证一下

验证:

我们在DynamicAdvisedInterceptor类的intercept方法打上断点

中间断点省略。。。。。

最终在TransactionAspectSupport类中的invokeWithinTransaction方法中看到了我们熟悉的代码

事实证明确实是test()方法执行完后锁释放了,但是事务没有提交导致下一个线程买票时候重复了。

解决问题:

知道了问题是怎么发生的,那就好解决了,把锁放到事务提交后执行不就ok了

controller层代码

import com.syl.cloud_2024.service.TransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class TransactionalController {

    @Autowired
    private TransactionalService transactionalService;

    @GetMapping("testA")
    public void testA() {

        for (int i = 1; i <= 50; i++){
            new Thread() {
                @Override
                public void run() {
                        transactionalService.test2();
                }
            }.start();
        }
    }
}

service层代码

public interface TransactionalService {

    /**
     *
     * @author yeYingXuan
     */
    void test();

    /**
     *
     * @author yeYingXuan
     */
    void test2();
}

service层实现代码

import com.syl.cloud_2024.bean.entity.TestA;
import com.syl.cloud_2024.mapper.TestAMapper;
import com.syl.cloud_2024.service.FarmService;
import com.syl.cloud_2024.service.TransactionalService;
import com.syl.cloud_2024.utils.RedisLockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class TransactionalServiceImpl implements TransactionalService {

    @Autowired
    FarmService farmService;
    @Autowired
    RedisLockService redisLockService;

    @Lazy
    @Autowired
    private TransactionalService transactionalService;

    @Autowired
    private TestAMapper testAMapper;



    @Override
    @Transactional(rollbackFor = Exception.class)
    public void test() {
        String lockKey = "lock:Api:test2";
        // 加锁并设置过期时间3秒,等待时常3秒
        boolean locked = redisLockService.tryLock(lockKey, Thread.currentThread().getName(), 5, TimeUnit.SECONDS, 5000);
        TestA oldTestA = testAMapper.selectById(1);
        int level = oldTestA.getLevel() - 1;
        log.info("我是线程{},我买完还剩余{}张高铁票", Thread.currentThread().getName(), level);
        TestA testA = new TestA();
        testA.setId(1);
        testA.setLevel(level);
        testAMapper.updateById(testA);
    }

    @Override
    public void test2() {
        String lockKey = "lock:Api:test2";
        try {
            // 加锁并设置过期时间3秒,等待时常3秒
            boolean locked = redisLockService.tryLock(lockKey, "locked", 3, TimeUnit.SECONDS, 3000);
            if (locked) {
                transactionalService.test();
            } else {
                throw new RuntimeException("稍后再试");
            }
        } finally {
            // 释放锁(只有当前线程持有锁时才释放锁)
            redisLockService.unlock(lockKey, "locked");
        }
    }
}

但是要注意一个问题,test2()方法不能加事务,如果加事务的话跟test()结果一样了,并且因为是本类调用,this不会走代理,所以这里自己注入一下自己。

验证结果:

结果正常。

两个小知识点:

1、@Transactional尽量和锁不要出现在同一方法。

2、调本地this方法不会走代理。

  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值