предисловие
Начиная с этой статьи, Лао Мао поделится с вами процессом эволюции, запертым в сценариях практического применения, через бизнес-сценарии в электронной коммерции. От одиночной блокировки Java к практике блокировки в распределенной среде.
Первое явление перепроданности
На самом деле, в бизнес-сценарии электронной коммерции будет такое запретное явление, то есть «перепроданность», так что же такое перепроданность? Например, инвентарь определенного продукта составляет всего 10, но в конечном итоге продано 15. Короче говоря, количество проданных продуктов превышает инвентарь самого продукта. «Перепроданность» приведет к тому, что продавец не доставит товар, а время доставки будет продлено, что вызовет споры между двумя сторонами сделки.
Давайте проанализируем причины этого явления: если есть только последняя позиция товара, пользователь А и пользователь Б видят товар одновременно, и в то же время добавляют в корзину для оформления заказа, в это время , два пользователя одновременно читают товар в инвентаре. Количество составляет одну штуку, и после каждого вычета памяти база данных обновляется. Поэтому возникает перепроданность.Давайте посмотрим на блок-схему:
решение
Когда мы сталкиваемся с вышеуказанными проблемами, как мы их решаем, когда используем один сервер? Давайте посмотрим на конкретные планы. Как упоминалось в предыдущем описании, когда мы вычитаем инвентарь, мы делаем это в памяти. Затем мы погрузим его в базу данных, чтобы обновить инвентаризацию.Мы можем передать приращение инвентаризации в базу данных, вычесть инвентарь, и приращение равно -1.Когда база данных выполняет оператор обновления для расчета инвентаризации, мы передаем оператор обновления.Блокировки строк решают проблемы параллелизма. (Блокировка строки базы данных: когда база данных обновляется, текущая строка блокируется, что является блокировкой строки. Описание старого кота здесь относительно простое. Заинтересованные партнеры могут спонтанно изучить блокировку базы данных). Давайте рассмотрим конкретный пример кода.
Код бизнес-логики выглядит следующим образом:
@Service
@Slf4j
public class OrderService {
@Resource
private KdOrderMapper orderMapper;
@Resource
private KdOrderItemMapper orderItemMapper;
@Resource
private KdProductMapper productMapper;
//购买商品id
private int purchaseProductId = 100100;
//购买商品数量
private int purchaseProductNum = 1;
@Transactional(rollbackFor = Exception.class)
public Integer createOrder() throws Exception{
KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
//校验库存
if (purchaseProductNum > currentCount){
throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
//计算剩余库存
Integer leftCount = currentCount -purchaseProductNum;
product.setCount(leftCount);
product.setTimeModified(new Date());
product.setUpdateUser("kdaddy");
productMapper.updateByPrimaryKeySelective(product);
//生成订单
KdOrder order = new KdOrder();
order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
order.setOrderStatus(1);//待处理
order.setReceiverName("kdaddy");
order.setReceiverMobile("13311112222");
order.setTimeCreated(new Date());
order.setTimeModified(new Date());
order.setCreateUser("kdaddy");
order.setUpdateUser("kdaddy");
orderMapper.insertSelective(order);
KdOrderItem orderItem = new KdOrderItem();
orderItem.setOrderId(order.getId());
orderItem.setProductId(product.getId());
orderItem.setPurchasePrice(product.getPrice());
orderItem.setPurchaseNum(purchaseProductNum);
orderItem.setCreateUser("kdaddy");
orderItem.setTimeCreated(new Date());
orderItem.setTimeModified(new Date());
orderItem.setUpdateUser("kdaddy");
orderItemMapper.insertSelective(orderItem);
return order.getId();
}
}
Из приведенного выше кода видно, что вычет инвентаря выполняется в памяти. Затем давайте взглянем на конкретный код модульного теста:
@SpringBootTest
class DistributeApplicationTests {
@Autowired
private OrderService orderService;
@Test
public void concurrentOrder() throws InterruptedException {
//简单来说表示计数器
CountDownLatch cdl = new CountDownLatch(5);
//用来进行等待五个线程同时并发的场景
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i =0;i<5;i++){
es.execute(()->{
try {
//等待五个线程同时并发的场景
cyclicBarrier.await();
Integer orderId = orderService.createOrder();
System.out.println("订单id:"+orderId);
} catch (Exception e) {
e.printStackTrace();
}finally {
cdl.countDown();
}
});
}
//避免提前关闭数据库连接池
cdl.await();
es.shutdown();
}
}
После выполнения кода давайте посмотрим на результат:
订单id:1
订单id:2
订单id:3
订单id:4
订单id:5
Очевидно, что хотя в базе данных имеется только один запас, генерируются пять записей заказа, как показано на следующем рисунке:
Это также приводит к феномену перепроданности, так как же мы можем решить эту проблему?
В монолитной архитектуре блокировка строки базы данных используется для решения проблемы перепроданности электронной коммерции.
Тогда, если это это решение, нам нужно погрузить наше действие по вычету запасов в нашу базу данных и использовать блокировку строк базы данных для решения проблемы одновременной работы в параллельных условиях.Давайте посмотрим на точки преобразования кода .
@Service
@Slf4j
public class OrderServiceOptimizeOne {
.....篇幅限制,此处省略,具体可参考github源码
@Transactional(rollbackFor = Exception.class)
public Integer createOrder() throws Exception{
KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
//校验库存
if (purchaseProductNum > currentCount){
throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
//在数据库中完成减量操作
productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());
//生成订单
.....篇幅限制,此处省略,具体可参考github源码
return order.getId();
}
}
Посмотрим на результаты выполнения
Из приведенных выше результатов мы обнаружили, что количество наших заказов по-прежнему составляет 5 заказов, но количество запасов в настоящее время больше не равно 0, а изменилось с 1 до -4. , это еще одно явление перепроданности. Давайте посмотрим на причины второго феномена перепроданности.
Второй феномен перепроданности
Вышеупомянутое на самом деле второе явление, так в чем же причина? На самом деле была проблема при проверке инвентаря.При проверке инвентаря, инвентарь проверялся одновременно.Пять потоков получили инвентарь одновременно, и обнаружили, что количество инвентаря было все 1, вызывая иллюзию достаточного инвентаря. В это время, поскольку существует блокировка строки для обновления во время операции записи, она будет вычитаться и выполняться по очереди, а логика проверки во время операции вывода отсутствует. Отсюда и это проявление перепроданности. Просто, как показано ниже:
Решение первое:
В монолитной архитектуре блокировка строки базы данных используется для решения проблемы перепроданности электронной коммерции. Для текущего случая, по сути, наше решение относительно простое, то есть после завершения обновления мы можем сразу проверить, больше ли количество инвентаря или равно 0. Если оно отрицательное, мы можем напрямую генерировать исключение. (Конечно, поскольку эта операция не требует знания блокировок, эта схема только предлагается, а не является реальной практикой кода)
Решение второе:
При проверке инвентаризации и вычете инвентаризации блокировка унифицируется, что делает ее атомарной операцией.При одновременном выполнении инвентаризация библиотеки будет прочитана и вычтена только при получении блокировки. Когда вычет закончится, отпустите замок, чтобы гарантировать, что инвентарь не будет вычтен отрицательно. Затем нам нужно использовать ключевые слова двух замков в java, упомянутых в предыдущем сообщении в блоге.synchronized
ключевые слова иReentrantLock
.
оsynchronized
Ключевые слова в предыдущем сообщении в блоге также упоминались, есть два способа блокировки и блокировки кодовых блоков, у нас есть время, чтобы посмотреть на код на практике, первый метод — это блокировка, конкретный код выглядит следующим образом:
//`synchronized`方法块锁
@Service
@Slf4j
public class OrderServiceSync01 {
.....篇幅限制,此处省略,具体可参考github源码
@Transactional(rollbackFor = Exception.class)
public synchronized Integer createOrder() throws Exception{
KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
//校验库存
if (purchaseProductNum > currentCount){
throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
//在数据库中完成减量操作
productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());
//生成订单
.....篇幅限制,此处省略,具体可参考github源码
return order.getId();
}
}
На данный момент мы смотрим на результаты операции.
[pool-1-thread-2] c.k.d.service.OrderServiceSync01 : pool-1-thread-2库存数1
[pool-1-thread-1] c.k.d.service.OrderServiceSync01 : pool-1-thread-1库存数1
订单id:12
[pool-1-thread-5] c.k.d.service.OrderServiceSync01 : pool-1-thread-5库存数-1
订单id:13
[pool-1-thread-3] c.k.d.service.OrderServiceSync01 : pool-1-thread-3库存数-1
На данный момент мы, очевидно, обнаружили, что проблема с данными все еще существует, так в чем же причина этого?
На самом деле, умные друзья действительно обнаружили, что данные, считанные нашим вторым потоком, по-прежнему равны 1, так почему? На самом деле все очень просто.Причина, по которой второй поток при чтении товарной инвентаризации равен 1, заключается в том, что транзакция предыдущего потока не была отправлена.Также мы ясно видим, что текущая транзакция в нашем методе находится вне блокировки . . . Поэтому возникает эта проблема, поэтому для этой проблемы мы можем фактически отправить отправку транзакции вручную, а затем поместить ее в блок кода блокировки. Конкретная модификация заключается в следующем.
public synchronized Integer createOrder() throws Exception{
//手动获取当前事务
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
platformTransactionManager.rollback(transaction);
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
log.info(Thread.currentThread().getName()+"库存数"+currentCount);
//校验库存
if (purchaseProductNum > currentCount){
platformTransactionManager.rollback(transaction);
throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
//在数据库中完成减量操作
productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());
//生成订单并完成订单的保存操作
.....篇幅限制,此处省略,具体可参考github源码
platformTransactionManager.commit(transaction);
return order.getId();
}
На этом этапе давайте посмотрим на результаты операции:
[pool-1-thread-3] c.k.d.service.OrderServiceSync01 : pool-1-thread-3库存数1
[pool-1-thread-5] c.k.d.service.OrderServiceSync01 : pool-1-thread-5库存数0
订单id:16
[pool-1-thread-4] c.k.d.service.OrderServiceSync01 : pool-1-thread-4库存数0
[pool-1-thread-1] c.k.d.service.OrderServiceSync01 : pool-1-thread-1库存数0
Согласно приведенным выше результатам, мы можем ясно видеть, что только первый поток считывает инвентаризацию 1, а все последующие потоки получают 0 инвентаризации. Давайте посмотрим на конкретную базу данных.
Очевидно, что наши запасы и объемы заказов в этой базе данных также верны.
Позадиsynchronized
кодовый замок иReentrantLock
Оставьте это друзьям, чтобы попытаться завершить это самостоятельно, конечно, старый кот уже написал соответствующий код. Конкретный адрес источника:GitHub.com/CatDaddy/Seeing-Article …
напиши в конце
В этой статье мы с друзьями поделимся процессом решения проблемы одного замка с помощью двух феноменов перепроданности в электронной коммерции. Конечно, использование такого типа блокировки не может охватывать jvm, и он не будет работать, если встретится несколько jvm, поэтому реализация распределенной блокировки будет рассмотрена в следующих статьях. Конечно, вам также поделятся этим на примере перепродажи в электронной коммерции.
Кроме того, я также надеюсь, что все не будут проституировать меня по пустякам.Если я могу помочь вам, я надеюсь получить ваши лайки и репост.