高并发下解决线程安全问题
在高并发的情境下,库存超卖成为了一个常见的问题。同时,为了提升用户体验和确保交易的公平性,实现一人一单的功能也变得至关重要。
建表
创建商品表和订单表
CREATE TABLE `goods` (
`id` int NOT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '商品名称',
`stock` int DEFAULT NULL COMMENT '库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `t_order` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单id',
`user_id` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '下单用户id',
`goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '下单商品',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
插入数据
INSERT INTO `test`.`goods` (`id`, `name`, `stock`) VALUES (1, '小米手机', 100);
库存超卖分析
下订单 controller
@RequestMapping("/{userId}")
@Transactional
public Result<Object> goods(@PathVariable String userId) {
// 查询商品库存
Goods goods = goodsService.getById(1);
if (goods.getStock()<1) return Result.fail(500, "库存不足");
// 减库存
goodsService.update().setSql("stock=stock-1").eq("id", goods.getId()).update();
// 下订单
Order order = Order.builder().goodsName("小米手机").userId(userId).build();
orderService.save(order);
return Result.success();
}
以上代码经过200个并发,结果库存为负数了,存在超卖的情况
这里由于多个线程读取到的库存值一样,导致同时执行更新操作导致库存超卖。
乐观锁解决库存超卖
乐观锁的关键是在修改时候,判断之前查询到的数据是否有被修改过。
由于是减库存,只需要判断库存是否大于0,代码修改如下
@RequestMapping("/{userId}")
@Transactional
public Result<Object> goods(@PathVariable String userId) {
// 减库存
boolean b = goodsService.update().setSql("stock=stock-1").eq("id", 1)
.gt("stock", 0)
.update();
if (!b) return Result.fail(500, "库存不足");
// 下订单
Order order = Order.builder().goodsName("小米手机").userId(userId).build();
orderService.save(order);
return Result.success();
}
悲观锁实现一人一单
要想实现一人一单的功能,需要对用户id进行上锁,保证同一时刻同一个用户只有一个线程可以下单。
加锁时需要注意以下两点
- 由于事务是加在方法上,
synchronized
如果加在方法内部会导致释放锁后事务还没提交,其他线程进入获取旧的结果导致并发安全问题。所以需要在方法外层加锁。 - 在Spring中,事务的实现方式是对当前类做了动态代理!用其代理对象去做事务处理! 所以我们要获取当前类的代理对象!否则事务失效
- 对字符串进行加锁,需要调用intern()方法,将字符串转换为字符串常量,才能保证多线程的同步
加入aspectj依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
启动类加上注解暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
service代码
public interface GoodsService extends IService<Goods> {
Result<Object> secKill(String userId);
Result<Object> createOrder(String userId);
}
service实现类代码如下
package com.wl.redislock.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.redislock.mapper.GoodsMapper;
import com.wl.redislock.pojo.Goods;
import com.wl.redislock.pojo.Order;
import com.wl.redislock.res.Result;
import com.wl.redislock.service.GoodsService;
import com.wl.redislock.service.OrderService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @Author 没有梦想的java菜鸟
* @Date 2024/1/8 17:37
* @Version 1.0
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper,Goods> implements GoodsService {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
public Result<Object> secKill(String userId) {
// 这里需要调用字符串的intern方法,将userId转换为字符串常量,这样才能保证多线程下的同步
synchronized (userId.intern()){
// 在Spring中,事务的实现方式是对当前类做了动态代理!用其代理对象去做事务处理!
// 所以我们要获取当前类的代理对象!否则事务失效
GoodsService proxy = (GoodsService) AopContext.currentProxy();
return proxy.createOrder(userId);
}
}
@Transactional
public Result<Object> createOrder(String userId) {
// 判断是否已经下过单
Integer count = orderService.lambdaQuery().eq(Order::getUserId, userId).count();
if (count > 0){
return Result.fail("用户已经购买过一次了!");
}
// 减库存
boolean b = goodsService.update().setSql("stock=stock-1").eq("id", 1)
.gt("stock", 0)
.update();
if (!b) return Result.fail(500, "库存不足");
// 下订单
Order order = Order.builder().goodsName("小米手机").userId(userId).build();
orderService.save(order);
return Result.success();
}
}
redis分布式锁解决集群模式下一人一单
在上面我们使用了synchronized
实现了一人一单的功能。但是值得思考的是,synchronized
只会再当前jvm进程生效,如果并发太大,需要搭建集群,那么使用synchronized
就无法实现一人一单的功能。