笔者最近赋闲,以前公务繁忙,几乎不怎么写博(主要还是懒),但考虑到现在什么样的公司都动不动就要求你有点博客,开源,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后,虽然依然大量的并发还在执行,但是因为不在更新表数据,所以控制台指示滚动如下信息,不再触发锁冲突:
验证此时数据库表:
整个场景模拟完毕,本示例如有不妥当之处,欢迎大咖在下面讨论并斧正。