项目中我们经常能见到一些并发问题,现对一些常见并发问题进行总结,知识结构不会很全,但比较实用。
- 基本概念
- 什么是并发问题
我们以记录网站的访问量为例,先看一下并发问题是如何产生的。
private Integer count=1;
private AtomicInteger atomicCount = new AtomicInteger(1);
/**
* 非线程安全方式
* @param request * @return */
@RequestMapping("/getCount")
public BaseResponse
}
我们通过jmeter模拟并发1000个请求,会发现我们得到的的值并不一定是1000。这里因为count++ 并不是线程安全的,所以在并发情况下会存在线程安全问题。
- 几组重要概念
同步VS异步
同步和异步通常用来形容一次方法调用。同步方法调用开始后,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
并发与并行
并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。
阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。
临界区
临界区用来表示公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。
- 常见并发问题解决方案
- Java代码层面
- 使用synchronized
_/**
*_ _使用synchronized
* @param_ request * @return */ @RequestMapping("/getCountWithSyn") public BaseResponse
- 使用Lock
- /** * 使用synchronized * @param request * @return */ @RequestMapping("/getCountWithSyn") public BaseResponse
visitCountWithSynchro(HttpServletRequest request){ synchronized (this){ count++; } return new BaseResponse<>(true, count); } - 使用原子操作类
- /** * 使用原子类 * @param request * @return */ @RequestMapping("/getCountWithAct") public BaseResponse
visitCountWithAct(HttpServletRequest request){ return new BaseResponse<>(true, atomicCount.getAndIncrement()); }
- 数据库层面
实际项目开发中,经常会遇到并发下单类似的场景,我们以商品购买下单为例,讲解一下如何通过数据库悲观锁乐观锁控制并发,本案例有一定的参考性,但其中代码比较简单,省去了很多逻辑校验,遇到类似场景请勿照搬。
所涉及表:
- 商品库存表:
create table T_STOCK
(
id NUMBER not null,
product_name VARCHAR2(255),
price NUMBER,
num NUMBER,
create_time TIMESTAMP(6),
version NUMBER
);
-- Add comments to the columns
comment on column T_STOCK.id
is 'id';
comment on column T_STOCK.product_name
is '商品名称';
comment on column T_STOCK.price
is '商品价格';
comment on column T_STOCK.num
is '商品数量';
comment on column T_STOCK.create_time
is '创建时间';
comment on column T_STOCK.version
is '版本号';
- 订单表:
-- Create table
create table T_ORDER
(
id NUMBER not null,
stock_id NUMBER,
product_name VARCHAR2(255),
num NUMBER,
user_name VARCHAR2(255)
);
-- Add comments to the columns
comment on column T_ORDER.id
is 'id';
comment on column T_ORDER.stock_id
is '库存id';
- 最Low实现(非线程安全)
日常实现中很容易出现超发或者库存数量扣为负数线下,先看下我们初学者很容易实现的方式。
Service层代码:
@Override
@Transactional public String dealOrder(String userName, Long num, Long stockId) { //查询stock Stock stock = stockDao.getById(stockId);
if(stock.getNum()>=num){
Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); //保存订单信息 orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); //更新库存信息 stockDao.update(stock); } return "success"; }
- 悲观锁
- @Override @Transactional public void dealOrderWithPessLock(String userName, Long num, Long stockId) { Stock stock = stockDao.getByIdForUpdate(stockId); if(stock.getNum()>=num){ Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); stockDao.update(stock); } }
Dao层
@Select("select * from t_stock where id=#{id} for update") @Results({ @Result(property = "id",column = "id"), @Result(property = "productName",column = "product_name"), @Result(property = "price",column = "price"), @Result(property = "num",column = "num"), @Result(property = "createTime",column = "create_time"), @Result(property = "version",column = "version")
})
Stock getByIdForUpdate(Long stockId);
适用场景:
比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
- 乐观锁
Service层:
@Override
@Transactional public void dealOrderWithOptLock(String userName, Long num, Long stockId) {
Stock stock = stockDao.getById(stockId);
if(stock.getNum()>=num){
Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); stock.setNum(stock.getNum() - num);
int i= stockDao.updateWithVersion(stock);
if(i==1){ orderDao.insert(order); } logger.info("更新库存成功条数:"+i); }else{ logger.error("库存不足"); }
}
Dao层:
@Update("update t_stock set num = #{num}, version =version+1 where id = #{id} and version=#{version}") int updateWithVersion(Stock stock);
适用场景:
比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
- 分布式锁层面
我们这里适用redis 做分布式锁控制,这里仅仅做分布式锁,并不做缓存使用。
Service层:
@Override
@Transactional public void dealOrderWithDisLock(String userName, Long num, Long stockId) {
RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3); //查询stock @Override
@Transactional public void dealOrderWithDisLock(String userName, Long num, Long stockId) {
RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3); //查询stock Stock stock = stockDao.getById(stockId);
if(stock.getNum()>=num){
Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); //保存订单信息 orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); //更新库存信息 stockDao.update(stock); logger.info("下单成功:"+userName+"--num:"+num); }else{ logger.error("库存不足,下单失败"); }
RedissLockUtil.unlock("lock"); }
- 性能比较
数据库悲观锁:
数据库乐观锁:
Redis分布式锁
- 总结:
在我们日常项目开发中,涉及并发控制的地方一般建议使用数据库乐观锁就可以解决,对于有些场景需要控制插入记录数量时,可以通过分布式锁去解决。