zoukankan      html  css  js  c++  java
  • 高并发下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没法同时执行的原因导致的。所以使用编程式事务是不存在上述问题的(我试过)。

  • 相关阅读:
    Spring配置文件命名空间
    JSTL之数字、日期格式化<fmt:formatNumber/>、<fmt:formatDate/>
    获取真实ip
    DES
    MD5
    手动导入maven包
    windows下redis主从复制设置
    linux系统上传图片读取权限设置
    linux系统项目查看进程
    linux上服务起的很慢解决方式
  • 原文地址:https://www.cnblogs.com/TravisGrady/p/10669840.html
Copyright © 2011-2022 走看看