高并发下Service层的写法

最近在项目里遇到一个坑,先上简易版的描述:每次从库里查询一下库存余量,每次购买一个商品。

数据库:

store为库存量。

service层代码:

@Override
    public synchronized void sell() {
        System.out.println("<======"+System.currentTimeMillis());
        
        //根据局id获取商品信息
        Goods goods = goodDao.findOne(1);
        //获取当前库存
        int store = goods.getStore();
        System.out.println(Thread.currentThread().getName()+" begin:"+store);
        
        
        if(store - 1 >= 0) {
            store = store -1;
            goods.setStore(store);
            //save当前余量
            Goods save = goodDao.save(goods);
            System.out.println(Thread.currentThread().getName()+" end:"+save.getStore());
        }
        System.out.println(System.currentTimeMillis()+"========>");
        
    }

在这段代码里,因为加了synchronized进行修饰,所以无论多少个线程过来,只会有一个线程对锁住的代码块进行操作,那么,库存始终减1,那么这样是没有问题的。

接下来,如果加入@Transactional,开启声明式事务,那么就会有坑了。

@Override
    @Transactional
    public synchronized void sell() {
        System.out.println("<======"+System.currentTimeMillis());
        
        //根据局id获取商品信息
        Goods goods = goodDao.findOne(1);//获取当前库存
        int store = goods.getStore();
        System.out.println(Thread.currentThread().getName()+" begin:"+store);
        
        
        if(store - 1 >= 0) {
            store = store -1;
            goods.setStore(store);
            //展示库存-1后的余量
            Goods save = goodDao.save(goods);
            //TODO 可能对其他表进行了操作....
            System.out.println(Thread.currentThread().getName()+" end:"+save.getStore());
        }
        System.out.println(System.currentTimeMillis()+"========>");
        
    }

由于加入了@Transactional,那么就会当做一个事务来进行处理。如果并发的去执行,那么会库存扣减不一致。原因在于,第一个线程执行完成以后,aop的方法还在继续,需要去commit,这个需要一定的时间。然后这个时候代码块已经走完了,释放了锁,那下一个线程过来去库里查,还是commit前的库存数量,所以,导致该问题。

解决办法是自定义一个查询方法,使用select ... for update的方式,给这条数据加上锁。JPA的repositry里的写法:

//PESSIMISTIC_WRITE:事务开始即获得数据库的锁
    @Lock(value=LockModeType.PESSIMISTIC_WRITE)
    @Query(value = "select t from Goods t where t.id =?1 ")
    Goods queryById(Integer id);

那么就ok了,原理是这样的: 在第一个线程进来的时候,开启了一个事务,给当前这行数据加了一个行锁,然后在代码执行到最后的时候,虽然jvm里的锁会释放,第二个线程会进来,但是会卡在select for update这里,因为第一个事务还没有提交,所以行锁还在。直到第一个事务提交了以后,第二个线程才会继续执行,查询到数据,这个时候的数据,一定是commit完成以后的数据了。那就不会有脏数据的发生。

这次问题的主要原因是JVM锁与@Transactional声明式事务aop没法同时执行的原因导致的。所以使用编程式事务是不存在上述问题的(我试过)。

原文地址:https://www.cnblogs.com/TravisGrady/p/10669840.html