Управление количеством товаров в базе данных
Извините, если этот вопрос может показаться наивным, но я столкнулся с ситуацией, когда мне нужно управлять количеством продуктов в базе данных магазина электронной коммерции.
Существует класс Product с целочисленной переменной productCount
, который обозначает количество доступных продуктов в базе данных, которое видимо для пользователей сайта. Теперь к этому классу обращаются несколько потоков или могут сказать несколько пользователей сайта электронной коммерции. Каждый добавляет или удаляет продукт в свою тележку.
Используемая структура ORM - это спящий режим
Пример кода
@Entity
@Table
class Product{
@Column
private int productCount;
public void addProductToCart(){
// decrements the product count by 1 & updates the database
}
public void removeTheProductFromTheCart(){
// increments the product count by 1 & updates the database
}
Как ясно из кода, мне нужно сохранить проверку concurrency на счете продукта в базе данных, чтобы предотвратить потерянные обновления.
Также, если несколько пользователей пытаются добавить только один левый продукт в базу данных. Какую пользовательскую корзину продукт следует добавить?
Я немного исследовал этот
Возможные пути, которые я нашел, были
-
Создание класса Singleton для Product. Это обеспечит доступность всего одного экземпляра продукта во всем приложении.
-
Синхронизировать методы addProductToCart
и removeTheProductFromTheCart
.
который позволит только одному потоку обновлять счетчик продуктов и обновлять db за раз.
-
Использование базы данных concurrency управляет применением некоторого уровня изоляции транзакции db, оптимистичной/пессимистической блокировки для productCount
. Я использую mysql, уровень изоляции по умолчанию - REPEATABLE_READ
.
Каким будет лучший подход к решению этого вопроса?
Ответы
Ответ 1
Для первых двух возможностей, которые вы рассматриваете, они работают только в том случае, если вы ограничены развертыванием только одного экземпляра приложения. Вы не можете управлять одиночками в нескольких экземплярах приложений, вы не можете синхронизировать их с несколькими JVM. Поэтому, если вы идете с одним из них, вы никогда не сможете развернуть несколько экземпляров вашего приложения в кластере или ферме или даже два экземпляра с балансировщиком нагрузки спереди. Таким образом, эти данные кажутся нежелательными, поскольку они работают только в режиме с одной точкой отказа.
Подход к подсчету продуктов из базы данных имеет то преимущество, что он остается в силе, поскольку ваше приложение масштабируется в нескольких экземплярах.
Вы можете подумать, что это будет только один экземпляр на одном сервере, чтобы я мог справиться с этим. Но в то время, когда вы создаете приложение, может быть не совсем ясно, как приложение будет развернуто (я был в ситуациях, когда мы не знали, какой план был до тех пор, пока приложение не было настроено в среде preprod), или на более позднем этапе может возникнуть причина изменить способ развертывания приложения; если ваше приложение имеет более чем ожидаемую нагрузку, тогда может оказаться полезным настроить второй блок.
Одна вещь, которая не очевидна для меня, - это то, насколько важно, чтобы количество продуктов было действительно правильным. В разных бизнес-доменах (авиабилеты, доставка) это обычное дело, и это может быть больше проблем, чем стоит сохранить 100% точный счет, особенно если он на раннем этапе процесса, например, добавление товара в магазин (по сравнению с тем моментом, когда клиент фактически совершает покупку). В то время, когда клиент покупает что-то, может возникнуть больше смысла, чтобы убедиться, что вы резервируете эти элементы транзакцией базы данных (или нет, например, переупорка).
В веб-приложениях обычно кажется, что низкий коэффициент конверсии из товаров в корзине до фактически купленных предметов. Имейте в виду, какой уровень точности для вашего счета подходит для вашего бизнес-домена.
Ответ 2
3. Использовать базу данных concurrency control
Почему?
-
1 и 2 ОК, если ваше приложение для электронной коммерции является абсолютно единственным способом изменить количество товаров. Это большой, если. В процессе ведения бизнеса и ведения инвентаризации магазину могут потребоваться другие способы обновления количества товаров, и приложение электронной коммерции может быть не идеальным решением. С другой стороны, база данных легче подключить к различным приложениям, которые помогают в процессе инвентаризации вашего магазина.
-
В продуктах базы данных обычно есть много отказобезопасных механизмов, поэтому, если что-то пойдет не так, вы можете отслеживать, какие транзакции преуспели, а какие нет, и вы можете вернуться к определенному моменту времени. Java-программа, плавающая в памяти, не имеет этого из коробки, вам придется развивать это самостоятельно, если вы сделали 1 или 2. Spring, а Hibernate и другие подобные вещи, безусловно, лучше, чем ничего, кроме сравнения того, что они предлагают и что предлагает база данных с точки зрения восстановления после некоторой электронной катастрофы.
Ответ 3
IMO поможет использовать обычный многоуровневый подход - не уверен, насколько радикальным будет это изменение, так как не знает размер/зрелость приложения, но будет продолжать и описывать его в любом случае, и вы можете выбрать, какие биты работоспособны.
Теория...
Services a.k.a. "business logic", "business rules", "domain logic" etc.
^
DAOs a.k.a. "Data Access Objects", "Data Access Layer", "repository" etc.
^
Entities a.k.a. "model" - the ORM representation of the database structure
^
Database
Полезно, чтобы сущности были отделены от слоя DAO, поэтому они просто простые единицы хранения, которые вы можете заполнить, сравнить и т.д., не включая методы, которые действуют на них. Таким образом, это просто представление класса того, что находится в базе данных, и в идеале не должно быть загрязнено кодом, который определяет, как они будут использоваться.
Уровень DAO предоставляет основные операции CRUD, которые позволяют этим объектам сохраняться, извлекаться, объединяться и удаляться без необходимости знать контекст, в котором это делается. Это одно место, где синглтоны могут быть полезны для предотвращения повторного создания нескольких экземпляров, но использование одноэлементности не подразумевает безопасности потоков. Лично я бы рекомендовал использовать Spring для этого (Spring beans по умолчанию одиночные), но предположим, что это можно сделать вручную, если это необходимо.
И уровень услуг заключается в том, где реализована "логика домена", то есть конкретные комбинации операций, необходимых вашему приложению для выполнения определенных функций. Здесь могут быть решены проблемы безопасности резьбы, и будут моменты, когда это необходимо, и время, когда это не так.
На практике...
Следуя этому подходу, вы можете получить что-то вроде этого (много опущены для краткости):
@Entity
@Table
public class Product {
@ManyToOne
@JoinColumn
private ShoppingCart shoppingCart;
}
@Entity
@Table
public class ShoppingCart {
@OneToOne
@JoinColumn
private User user;
@OneToMany(mappedBy = "shoppingCart")
private Set<Product> products;
}
public class ShoppingCartDao { /* persist, merge, remove, findById etc. */ }
@Transactional
public class ProductService() {
private ConcurrentMap<Integer, Integer> locks =
new ConcurrentHashMap<Integer, Integer>();
public void addProductToCart(final int productId, final int userId) {
ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);
Product product = productDao.findById(productId);
synchronized(getCacheSyncObject(productId)) {
if (product.shoppingCart == null) {
product.setShoppingCart(shoppingCart);
} else {
throw new CustomException("Product already reserved.");
}
}
}
public void removeProductFromCart(final int productId, final int userId) {
ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);
Product product = productDao.findById(productId);
if (product.getShoppingCart() != shoppingCart) {
throw new CustomException("Product not in specified user cart.");
} else {
product.setShoppingCart(null);
}
}
/** @See http://stackoverflow.com/questions/659915#659939 */
private Object getCacheSyncObject(final Integer id) {
locks.putIfAbsent(id, id);
return locks.get(id);
}
}
Ответ 4
Правильный способ сделать это - использовать блокировки базы данных, поскольку он предназначен для этой работы. И если вы используете спящий режим, это довольно просто с LockRequest:
Session session = sessionFactory.openSession()
Transaction transaction;
boolean productTaken = false;
try {
transaction = session.beginTransaction();
Product product = session.get(Product.class, id);
if (product == null)
throw ...
Session.LockRequest lockRequest = session.buildLockRequest(LockOptions.UPGRADE);
lockRequest.lock(product);
productTaken = product.take();
if (productTaken) {
session.update(product);
transaction.commit();
}
} finally {
if (transaction != null && transaction.isActive())
transaction.rollback();
session.close();
}
Здесь мы извлекаем продукт из базы данных для обновления, который предотвращает любые параллельные обновления.
Ответ 5
Позвольте оценить три варианта.
1.Создание одноэлементного класса для Product. Это обеспечит доступность всего одного экземпляра продукта во всем приложении.
Единственный экземпляр для продукта - это хорошо. Но если вы предлагаете продукт типа Mobile с количеством 20, все равно вы должны increment
количество товаров (статическая переменная) на addProductToCart
и decrement
количество товаров на removeTheProductFromTheCart
. Тем не менее вам нужно синхронизировать доступ к этому изменяемому счету или обновить базу данных и прочитать количество продуктов.
2. Синхронизируйте метод addProductToCart и removeTheProductFromTheCart. который позволит только одному потоку обновлять счетчик продуктов и обновлять db за раз.
Это одно решение, но я предпочитаю третий: удалить синхронизацию в приложении и обеспечить согласованность данных на уровне базы данных.
3. Используйте базу данных concurrency, чтобы применить некоторый уровень изоляции транзакций db, оптимистичную/пессимистичную блокировку для productCount. Я использую mysql, уровень изоляции по умолчанию - REPEATABLE_READ.
Отложить согласованность с базой данных вместо приложения. Но вы должны использовать READ COMMITTED
для уровня изоляции вместо REPEATABLE_READ
Посмотрите на статью
ПРОЧИТАЙТЕ КОМИТЕТ
A somewhat Oracle-like isolation level with respect to consistent (nonlocking) reads
: Каждое последовательное чтение, даже в пределах одной транзакции, устанавливает и считывает собственный свежий снимок.
Ответ 6
Как я понял, тележки также сохраняются в db? И в качестве конечного результата тоже покупали продукты.
продукт:
[{id:1, count:100}]
cart:
[{user_id:1, product_id:1}, {user_id:2, product_id:1}]
купил:
[{user_id:3, product_id:1}]
Затем вы можете получить счетчик для продукта
select count as total from product where id = 1
select count(*) as in_cart from cart where product_id = 1
select count(*) as total_bought from bought where product_id = 1
теперь вы можете показать окончательный счет
int count = total - (in_cart + total_bought);
Таким образом, вы гарантируете, что не будет превышать или обходить приращения/сокращения. Наконец, помимо проверки кода для счета, вы также можете добавить триггер на уровне БД, который проверяет общий счетчик, и если продукт можно вставить в телеги или купить табуляцию.
Если количество вашего продукта меняется ежедневно, я имею в виду, что вчера у вас было 100 продуктов и было продано 20, сегодня поступило 50 продуктов, поэтому у вас должно быть 150 - 20 проданных графов, что составляет 130. Чтобы иметь хорошие отчеты, вы можете ежедневно производить подсчет продукта. как
[
{id:1, count:100, newly_arrived: 0, date: 23/02/2016},
{id:1, count:80, newly_arrived: 50, date: 24/02/2016}
]
тогда ваши запросы будут меняться, как
select count+newly_arrived as total from product where id = 1 and date = today
select count(*) as in_cart from cart where product_id = 1 and date = today
select count(*) as total_bought from bought where product_id = 1 and date = today
для этого вам нужно только вставлять новые данные о продукте в полночь 00:00:00, а когда ваши новые продукты поступят утром, вы можете обновлять new_arrived без вмешательства каких-либо операций вставки или удаления на телегах или купленных таблицах. и вы можете иметь подробные отчеты легко без сложных запросов к запросу:))
Ответ 7
Если вам нужно использовать реляционную базу данных (а не хранилище значений ключей, например,). Я бы очень рекомендовал сделать это как можно ближе к хранилищу, чтобы избежать блокировок и конфликтов и получить максимальную производительность, насколько это возможно.
С другой стороны, это звучит как типичный сценарий, когда несколько узлов работают над одной базой данных, что также создает проблемы с задержкой из-за обработки сеанса Hibernate.
В конце вам нужны утверждения типа
UPDATE product SET productCount = productCount + 1 WHERE id = ?
Для выполнения транзакции может использоваться простой оператор JDBC или метод репозитория JPA с аннотацией @Query.
Чтобы сделать Hibernate осведомленным об изменениях, вы можете использовать Session.refresh()
для продукта после такой операции.
Ответ 8
Лучший способ сделать это в веб-приложении электронной коммерции, используя Очередь сообщений. Вы можете использовать RabbitMQ,
-
Создайте очередь на rabbitMQ и сохраните все запросы здесь, чтобы уменьшить количество продуктов
-
из-за очереди все запросы будут обрабатываться один за другим, поэтому не будет конфликтов с целью сокращения продукта.
Ответ 9
Это не столько технический вопрос, сколько вопрос процесса. Лично я бы не уменьшал инвентарь, пока кто-то фактически не купил продукт. Добавление элемента в корзину покупок является индикатором того, что они могут его купить. Не то, чтобы они имели.
Почему бы не уменьшить количество инвентаризации только тогда, когда платеж успешно обработан? Оберните все в транзакции базы данных. Если 10 человек добавляют один и тот же предмет в свою корзину, и осталось всего 1 левый, то первый будет выигрывать. 9 человек будут расстроены, они не платят быстрее.
Ссылка на авиалинии отличается. Они выполняют временные ограничения, но это намного сложнее, чем управление запасами. Это, и они работают с жидким бассейном мест, которые обезьяны обещают, поскольку инвентарь становится скудным.