Spring boot使用Timestamp乐观锁方式的高并发场景处理完整示例

笔者最近赋闲,以前公务繁忙,几乎不怎么写博(主要还是懒),但考虑到现在什么样的公司都动不动就要求你有点博客,开源,git啥的证明下自己,就随便班门弄斧折腾点啥吧,望各位读者多多给予支持!
JPA方式下,使用version方式的乐观锁机制,是网上阐述比较多的资源,虽然也有部分资料提及Timestamp时间戳的方式,但很多最后其实也都使用了version的方式,采取version的乐观锁方式,固然不错,但是数据库表设计里往往必须多余出这一个单独的version字段(笔者有点强迫症),未免不美,实际场景中,往往表列里会有类似时间戳一样的字段,比如gmt_modified(阿里Java规范里的常用数据库字段规范),用来记录行数据最后一次的更新时间,本文就是讲述如何使用该列来实现高并发场景下的乐观锁的处理。(BTW,乐观锁是啥请自行百度)
本文所有示例都是在Spring boot框架下进行的,版本是2.1.X

相关依赖库如下:

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-actuator')
    implementation('org.springframework.boot:spring-boot-starter-cache')
    implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-test')
    implementation('org.springframework.session:spring-session-core')
    compile('com.alibaba:druid:1.1.9')
    compile('com.alibaba:fastjson:1.2.47')
    compile('com.google.guava:guava:25.0-jre')
    compile('org.apache.commons:commons-text:1.6')
    compile('io.springfox:springfox-swagger2:2.8.0')
    compile('io.springfox:springfox-swagger-ui:2.8.0')
    compile('org.springframework.data:spring-data-elasticsearch:3.1.3.RELEASE')
    compile('com.aliyun.oss:aliyun-sdk-oss:3.3.0')
    compile('commons-fileupload:commons-fileupload:1.3.3')
    //compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE')
    compile('mysql:mysql-connector-java')
    compileOnly('org.springframework.boot:spring-boot-configuration-processor')
}

数据表

以常用的商品表为例,表结构如下:
在这里插入图片描述
注意字段设置为根据当前实际戳更新,默认值为:CURRENT_TIMESTAMP,插入一条测试数据用于验证我们的场景
在这里插入图片描述
商品是一本书《JPA高并发处理图文详细》,价格是30元,库存3000本

Entity

/**
 * @ClassName goods
 * @Description 商品表
 * @Author wangd
 * @Create 2018-12-25 11:28
 */
@Entity
@Table(name = "goods")
@Proxy(lazy = false)
public class Goods {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "name")
    private String name;

    @Column(name = "price")
    private int price;

    @Column(name = "stock")
    private int stock;

    @Version //版本控制注解
    @Column(name = "gmt_modified",columnDefinition="timestamp")//注解列名和列类型
    @Source(value = SourceType.DB) //注解值来自数据库
    private java.sql.Timestamp gmtModified;


    public long getId() {
        return id;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Timestamp getGmtModified() {
        return gmtModified;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
}

DAO

因为示例简单,可直接配置即可,不需要增加额外代码

/**
 * @InterFaceName GoodsDao
 * @Description 商品DAO
 * @Author wangd
 * @Create 2018-12-25 11:32
 */
public interface GoodsDao extends JpaRepository<Goods, Long> {

}

Service

/**
 * @InterFaceName GoodsService
 * @Description 商品处理接口
 * @Author wangd
 * @Create 2018-12-25 11:34
 */
public interface IGoodsService {
    /**
     * 更改商品库存
     * @param id
     * @param decStock 减去库存数量
     * @return
     */
    Goods decreaseStock(long id,int decStock) throws InterruptedException;
}

service的接口实现类

/**
 * @ClassName GoodsServiceImpl
 * @Description 商品服务接口实现
 * @Author wangd
 * @Create 2018-12-25 11:36
 */
@Service
@Slf4j
public class IGoodsServiceImpl implements IGoodsService {
    @Autowired
    private GoodsDao goodsDao;

    @Override
    public Goods decreaseStock(long id, int decStock) throws InterruptedException{
        //首先获取商品信息
        Goods oldGood=goodsDao.getOne(id);
        try {
            //减库存
            if (oldGood.getStock() - decStock >= 0) {
                oldGood.setStock(oldGood.getStock() - decStock);
                return goodsDao.saveAndFlush(oldGood);
            }
        }catch (Exception e){
            throw new InterruptedException("删减库存"+decStock+"失败!并发冲突!");
        }
        //返回null值表示库存已被清空,实际场景可自行处理,这里只是示例
        return null;
    }
}

基本的处理框架具备了,下面就需要验证我们的乐观锁是否起到作用,这里就要用到JDK1.5以后强大的一个java包concurrent,这个包提供了我们极为简洁且极为强大的并发支持,具体这个包的介绍,请自行参悟,这里不细讲。
下面构建一个JunitTest测试类

/**
 * @ClassName IGoodsServiceTest
 * @Description 测试
 * @Author wangd
 * @Create 2018-12-25 12:09
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@Slf4j
public class IGoodsServiceTest {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    @Autowired
    IGoodsService goodsService;

    @Test
    public void testDecreaseStock(){
        try {
            ExecutorService executorService = Executors.newCachedThreadPool();
            //信号量,此处用于控制并发的线程数
            final Semaphore semaphore = new Semaphore(threadTotal);
            //闭锁,可实现计数器递减
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                //使用lambada方式执行线程
                executorService.execute(() -> {
                    try {
                        //执行此方法用于获取执行许可,当总计未释放的许可数不超过200时,
                        //允许通行,否则线程阻塞等待,直到获取到许可。
                        semaphore.acquire();
                        //调用goodService的修改库存方法
                        Goods goods=goodsService.decreaseStock(1,25);
                        if(goods==null){
                            log.info("库存已经清空!");
                        }
                        //释放许可
                        semaphore.release();
                    } catch (InterruptedException e) {
                        //这里捕获IGoodsService抛出的中断异常,实际场景中可执行数据回滚操作
                        log.error("Exception", e);
                        e.printStackTrace();
                    }catch(Exception e){
                        log.error("exception", e);
                        e.printStackTrace();
                    }
                    //闭锁减一
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();//线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执行
            executorService.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行这个测试类,观察控制台输出,及数据库记录,可看到如下输出:
在这里插入图片描述
跟踪断点,检查是否是锁冲突导致的异常:
在这里插入图片描述
证明捕获了异常ObjectOptimisticLockingFailureException异常,而这个异常就是因为锁冲突造成的。而正是goodsDao.saveAndFlush(oldGood);这句触发了锁冲突弹出了我们指示的异常,在库存被删减到0后,虽然依然大量的并发还在执行,但是因为不在更新表数据,所以控制台指示滚动如下信息,不再触发锁冲突:

在这里插入图片描述
验证此时数据库表:

在这里插入图片描述

整个场景模拟完毕,本示例如有不妥当之处,欢迎大咖在下面讨论并斧正。

猜你喜欢

转载自blog.csdn.net/blackhost/article/details/85254928