使用单体锁和分布式锁解决超卖问题

使用单体锁和分布式锁解决超卖问题

超卖现象演示


@SpringBootTest
@RunWith(SpringRunner.class)
public class DistributeLockApplicationTests {

    @Autowired
    private OrderService orderService;


    /**
     * 模拟超卖
     */
    @Test
    public void concurrentOrder() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                try {
                    //等待5个线程执行到await时,再去执行后面得到步骤; 让所有的线程在等待等到某一刻再去执行,实现并发的效果
                    cyclicBarrier.await();

                    Integer orderId = orderService.createOrder();
                    System.out.println("订单id: " + orderId + "  " + Thread.currentThread().getName());

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });

            //等到5个线程执行完毕后,再去结束主线程,不这样做数据库连接会被关闭
            countDownLatch.await();

            //关闭线程池
            executorService.shutdown();

        }


    }
}

创建订单相关数据的方法如下:

    @Transactional(rollbackFor = Exception.class)
    public Integer createOrder() throws Exception {
        Product product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product == null) {
            throw new Exception("购买商品:" + purchaseProductId + "不存在");
        }

        //商品当前库存
        Integer currentCount = product.getCount();

        //校验库存
        if (purchaseProductNum > currentCount) {
            throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
        }

        //计算剩余库存
        int leftCount = currentCount - purchaseProductNum;
        //更新库存
        product.setCount(leftCount);

        // 将提交的数据添加到数据库中.
        product.setUpdateTime(new Date());
        product.setUpdateUser("xxx");
        productMapper.updateByPrimaryKeySelective(product);

        //创建订单
        Order order = new Order();
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);
        order.setReceiverName("xxx");
        order.setReceiverMobile("13311112222");
        order.setCreateTime(new Date());
        order.setCreateUser("xxx");
        order.setUpdateTime(new Date());
        order.setUpdateUser("xxx");
        orderMapper.insertSelective(order);

        //创建订单中的
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("xxx");
        orderItem.setCreateTime(new Date());
        orderItem.setUpdateTime(new Date());
        orderItem.setUpdateUser("xxx");
        orderItemMapper.insertSelective(orderItem);
        return order.getId();
    }

单体锁

数据库update行锁

数据库在更新一行数据时,会自动对这一行数据加锁,借助这个特性,我们可以解决并发问题。
将原来在程序中更新库存改成直接在数据库中更新库存。传入

    /**
     * 使用数据库update行锁处理
     * 数据库在更新某一行记录的时候会这一行加锁,一个线程处理完了,下一个线程才能继续处理
     */
    @Transactional(rollbackFor = Exception.class)
    public Integer createOrderByUpdateLock() throws Exception {
        Product product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product == null) {
            throw new Exception("购买商品:" + purchaseProductId + "不存在");
        }

        //商品当前库存
        Integer currentCount = product.getCount();

        //校验库存
        if (purchaseProductNum > currentCount) {
            throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
        }

//        //计算剩余库存
//        int leftCount = currentCount - purchaseProductNum;
//        //更新库存
//        product.setCount(leftCount);
//
//        // 将提交的数据添加到数据库中.
//        product.setUpdateTime(new Date());
//        product.setUpdateUser("xxx");
//        productMapper.updateByPrimaryKeySelective(product);

        //直接写入剩余库存,而不是在内存中计算出剩余库存后再写入数据库
        productMapper.updateProductCount(purchaseProductNum, "xxx", new Date(), product.getId());

        //创建订单
        Order order = new Order();
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);//待处理
        order.setReceiverName("xxx");
        order.setReceiverMobile("13311112222");
        order.setCreateTime(new Date());
        order.setCreateUser("xxx");
        order.setUpdateTime(new Date());
        order.setUpdateUser("xxx");
        orderMapper.insertSelective(order);

        //创建订单中的
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("xxx");
        orderItem.setCreateTime(new Date());
        orderItem.setUpdateTime(new Date());
        orderItem.setUpdateUser("xxx");
        orderItemMapper.insertSelective(orderItem);
        return order.getId();
    }

通过synchronzied关键字

将校验库存以及扣减库存的代码放在synchroinzed代码块中;但是需要自己手动管理事务。如果使用事务注解,第一个线程进入执行完后,事务未提交,第二个线程执行时获取到的库存还是1,这样还是会有并发问题。

    /**
     * 通过synchronized关键字
     */
    public Integer createOrderBySynchronized1() throws Exception {
        Product product = null;
        synchronized (OrderService.class) {
            product = productMapper.selectByPrimaryKey(purchaseProductId);
            if (product == null) {
                throw new Exception("购买商品:" + purchaseProductId + "不存在 " + Thread.currentThread().getName());
            }

            //商品当前库存
            Integer currentCount = product.getCount();

            //校验库存
            if (purchaseProductNum > currentCount) {
                throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买 " + Thread.currentThread().getName());
            }
            //计算剩余库存
            int leftCount = currentCount - purchaseProductNum;
            //更新库存
            product.setCount(leftCount);

            // 将提交的数据添加到数据库中.
            product.setUpdateTime(new Date());
            product.setUpdateUser("xxx");
            productMapper.updateByPrimaryKeySelective(product);
        }

        //创建订单
        Order order = new Order();
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);//待处理
        order.setReceiverName("xxx");
        order.setReceiverMobile("13311112222");
        order.setCreateTime(new Date());
        order.setCreateUser("xxx");
        order.setUpdateTime(new Date());
        order.setUpdateUser("xxx");
        orderMapper.insertSelective(order);

        //创建订单中的
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("xxx");
        orderItem.setCreateTime(new Date());
        orderItem.setUpdateTime(new Date());
        orderItem.setUpdateUser("xxx");
        orderItemMapper.insertSelective(orderItem);
        return order.getId();
    }

ReentrantLock

   /**
     * 通过ReentrantLock锁解决并发问题
     */
    public Integer createOrderByReentrantLock() throws Exception {
        Product product = null;

        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);

        reentrantLock.lock();
        try {
            product = productMapper.selectByPrimaryKey(purchaseProductId);
            if (product == null) {
                throw new Exception("购买商品:" + purchaseProductId + "不存在");
            }

            //商品当前库存
            Integer currentCount = product.getCount();

            //校验库存
            if (purchaseProductNum > currentCount) {
                throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
            }

            //计算剩余库存
            int leftCount = currentCount - purchaseProductNum;
            //更新库存
            product.setCount(leftCount);

            // 将提交的数据添加到数据库中.
            product.setUpdateTime(new Date());
            product.setUpdateUser("xxx");
            productMapper.updateByPrimaryKeySelective(product);

            platformTransactionManager.commit(transaction);
        } catch (Exception e) {
            platformTransactionManager.rollback(transaction);
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }

        TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);

        //创建订单
        Order order = new Order();
        assert product != null;
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);
        order.setReceiverName("xxx");
        order.setReceiverMobile("13311112222");
        order.setCreateTime(new Date());
        order.setCreateUser("xxx");
        order.setUpdateTime(new Date());
        order.setUpdateUser("xxx");
        orderMapper.insertSelective(order);

        //创建订单中的
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("xxx");
        orderItem.setCreateTime(new Date());
        orderItem.setUpdateTime(new Date());
        orderItem.setUpdateUser("xxx");
        orderItemMapper.insertSelective(orderItem);

        platformTransactionManager.commit(transaction1);

        return order.getId();
    }

分布式锁

在分布式集群的环境中,上面的单体锁会失效,此时需要通过分布式锁去处理并发问题。

数据库update锁

通过下面的sql给数据加锁,其它请求去更改数据时会被阻塞。

select... for update

Controller中的代码

    @Autowired
    private DistributeLockMapper distributeLockMapper;

    @RequestMapping("/test1")
    public String test1() throws Exception {
        log.info("进入方法..");
        DistributeLock lock = distributeLockMapper.selectLockByCode("demo");
        if (lock == null) {
            throw new Exception("找不到锁");
        }
        log.info("获取到了锁");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "执行完成";
    }

mapper文件中的代码

    <select id="selectLockByCode" resultType="com.lock.distribute_lock.model.DistributeLock">
        select * from distribute_lock where business_code = #{code} for update;

    </select>

redis实现分布式锁

Redis实现分布式锁的核心命令:

SETNX key value

SETNX 命令的作用是:如果指定的 key 不存在,则创建并为其设置值,然后返回状态码 1;如果指定的 key 存在,则直接返回 0。如果返回值为 1,代表获得该锁;此时其他进程再次尝试创建时,由于 key 已经存在,则都会返回 0 ,代表锁已经被占用。

当获得锁的进程处理完成业务后,再通过 del 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。

封装的RedisLock工具类

@Slf4j
public class RedisLock implements AutoCloseable{

    private RedisTemplate redisTemplate;

    private String key;

    private String value;

    private Long expireTime;

    public RedisLock(RedisTemplate redisTemplate, String key, Long expireTime) {
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.value = UUID.randomUUID().toString();
        this.expireTime = expireTime;
    }

    /**
     * 获取分布式锁
     * @return
     */
    public boolean getLock() {
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValu = redisTemplate.getValueSerializer().serialize(value);
            //执行setNx操作
            Boolean result = redisConnection.set(redisKey, redisValu, expiration, setOption);
            return result;
        };

        //获取分布式锁
        Boolean lock  = (Boolean) redisTemplate.execute(redisCallback);

        return lock;
    }


    public boolean unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
        List<String> keys = Arrays.asList(key);
        Boolean result = (Boolean) redisTemplate.execute(redisScript, keys, value);
        log.info("释放锁的结果: " + result);
        return result;
    }


    @Override
    public void close() throws Exception {
        unLock();
    }
}

测试代码

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/redis-lock")
    public String redisLock() {
        log.info("redislock begin....");

        try (RedisLock redisLock = new RedisLock(redisTemplate, "redisKey", 30L)) {
            if (redisLock.getLock()) {
                log.info("获取了锁!!");
                //模拟业务操作
                Thread.sleep(15000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("redis lock end");

        return "方法执行完成";
    }

参考

  1. 深入解析 Redis 分布式锁原理

你可能感兴趣的:(JavaEE,分布式,java,数据库,mysql,java-ee,spring,boot)