高并发下解决线程安全问题

发布时间 2024-01-09 16:51:11作者: 没有梦想的java菜鸟

​ 在高并发的情境下,库存超卖成为了一个常见的问题。同时,为了提升用户体验和确保交易的公平性,实现一人一单的功能也变得至关重要。

建表

创建商品表和订单表

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个并发,结果库存为负数了,存在超卖的情况

image

这里由于多个线程读取到的库存值一样,导致同时执行更新操作导致库存超卖。

image

乐观锁解决库存超卖

乐观锁的关键是在修改时候,判断之前查询到的数据是否有被修改过。

image

由于是减库存,只需要判断库存是否大于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就无法实现一人一单的功能。