一、数据库设计
订单表
CREATE TABLE t_order
(
id
int(11) NOT NULL AUTO_INCREMENT,
inventory
int(11) NOT NULL,
order_no
varchar(255) DEFAULT NULL,
user_name
varchar(255) DEFAULT NULL,
good_id
int(11) DEFAULT NULL,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=39668 DEFAULT CHARSET=utf8mb4;
库存表
CREATE TABLE t_inventory
(
id
bigint(20) NOT NULL,
good_name
varchar(255) DEFAULT NULL,
inventory
int(11) NOT NULL,
good_id
int(11) DEFAULT NULL,
version
int(11) DEFAULT NULL,
PRIMARY KEY (id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
一、模拟用户下单部分代码
每个用户一单,下单成功后,减库存,下单部分代码如下
dao层
Controller层
二、并发测试(无锁)
1000人5秒内 持续10次,
2.1、jmeter测试结果如下
2.2、数据库结果
2.2.1、库存剩余
2.2.2、订单总记录
订单明细部分截图
2.3、分析:
从性能上讲,最长返回时间9秒左右,最短11毫秒,平均为1.7秒,大部集中在2秒左右
从订单总数上看,总共10000个人下单,都下单成功,符合预期,
从剩余库存上看,结果不对,正确结果应该是减少10000,而结果是9997685,少扣减了7685件
三、解决方案:
3.1 数据库加悲观锁
悲观锁顾名思义就是悲观的认为自己操作的数据都会被其他线程操作,所以就必须自己独占这个数据,可以理解为”独占锁“。在java中synchronized和ReentrantLock等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁
利用SQL行锁解决并发问题
行锁就是操作数据的时候把这一行数据锁住,其他线程想要读写必须等待,但同一个表的其他数据还是能被其他线程操作的。只要在需要查询的sql后面加上for update,就能锁住查询的行,特别要注意查询条件必须要是索引列,如果不是索引就会变成表锁,把整个表都锁住。
现在在原有的代码的基础上修改一下,先在InventoryDao增加一个手动写sql查询方法。代码如下
package com.test.dao;
import com.test.entity.Inventory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
利用JPA自带行锁解决并发问题
对于刚才提到的在sql后面增加for update,JPA有提供一个更优雅的方式,就是@Lock注解,这个注解的参数可以传入想要的锁级别。
现在在ArticleRepository中增加JPA的锁方法,其中LockModeType.PESSIMISTIC_WRITE参数就是行锁。
/**
然后把service层的服务改下
public Inventory find(Long id){
//List list = inventoryDao.selectOneById(1L);
/**
jpa自带查询,没有锁,
/
//Inventory inventory = inventoryDao.findById(id);
/
*自定义查询,带锁
*/
return inventoryDao.selectOneById(id);
}
3.1.1 压测结果(悲观锁)
3.1.1.1 jmeter 测试结果如下
3.1.1.2 数据库结果
3.1.1.2.1 库存剩余
3.1.1.2.2 订单总记录
部分截图
3.2 数据库加乐观锁
乐观锁顾名思义就是特别乐观,认为自己拿到的资源不会被其他线程操作所以不上锁,只是在插入数据库的时候再判断一下数据有没有被修改。所以悲观锁是限制其他线程,而乐观锁是限制自己,虽然他的名字有锁,但是实际上不算上锁,只是在最后操作的时候再判断具体怎么操作。
乐观锁通常为版本号机制或者CAS算法
利用SQL实现版本号解决并发问题
版本号机制就是在数据库中加一个字段当作版本号,比如我们加个字段version。那么这时候拿到Article的时候就会带一个版本号,比如拿到的版本是1,然后你对这个Article一通操作,操作完之后要插入到数据库了。发现哎呀,怎么数据库里的Article版本是2,和我手里的版本不一样啊,说明我手里的Article不是最新的了,那么就不能放到数据库了。这样就避免了并发时数据冲突的问题。
所以我们现在给t_inventory表加一个字段version
接着在InventoryDao增加更新的方法,注意这里是更新方法,和悲观锁时增加查询方法不同。
/**
Service 屋增加 方法
/**
jpa的代码
@Version
private int version;
dao方法和悲观锁一样,不变,这种方式是非侵入式的,推荐使用
3.2.1 压测结果(乐观锁)
3.2.1.1 jmeter测试结果如下
数据库结果 符合预期,
3.3 综合结果分析
在秒内1万请求的压力测试,jpa中的悲观锁与乐观锁的详情对比下如下
悲观锁
乐观锁
结论,
从平均响应时间上看 乐观锁是悲观锁的2倍,
从95%line上看,乐观锁是悲观锁的2.3倍
从90%line上看,乐观锁是悲观锁的2.5倍
…
其它的对比,童鞋们可以仔细看,无论怎么看乐观锁性能都比百悲观锁大很多,这个测试跑的本地数据库,两个表的数据量都不大,如果两表的数据超过千万,在网络环境里,结果可能还有出入,有兴趣的同学可以试下,有问题童鞋欢迎留言