前段时间,我们实施了一个仓库管理应用程序,用于跟踪我们在商店中拥有的每种产品的数量。我们解决了使用数据库锁(选择更新)并发访问数据的问题,但是当许多客户尝试从同一个商店消耗产品数量时,这种方法导致性能不佳。请注意,我们只管理一小组产品类型(少于10个),因此并发程度可能很高(同样,我们也不关心库存重新填充)。我们考虑将每个资源数量分成较小的#34;桶#34;但是这种方法可能导致客户试图消耗大于每个桶容量的数量的饥饿:我们应该管理桶合并等等。 .. 我的问题是:这个问题有一些广泛接受的解决方案吗?我也寻找学术文章,但主题似乎太宽泛了。
P.S。 1: 我们的应用程序在集群环境中运行,因此我们不能依赖于应用程序并发控制。 问题的目的是找到一种算法,以不同于单行的方式构建和管理数据,但保留db事务(使用锁或不使用锁)的所有优点。
P.S。 2:对于您的信息,我们管理着大量类似的仓库,示例侧重于单个仓库,但我们将所有数据保存在一个db中(价格都相同,等等)。
答案 0 :(得分:0)
编辑:如果您使用可以在多个进程/服务器之间进行协调的排队程序,则下面的设置仍将在群集上运行,例如: RabbitMQ
您还可以使用仅使用数据库的更简单的排队算法,其缺点是需要轮询(而像RabbitMQ这样的系统允许线程阻塞,直到消息可用)。创建一个Requests表,其中包含用作唯一requestId
的列(例如,随机UUID),该列充当主键,timestamp
列,respourceType
列和整数{{1列。您还需要一个日志表,其中包含唯一的requestedQuantity
列作为主键,requestId
列,timestamp
列,整数resourceType
列,以及布尔/ tinyint /无论requestQuantity
列。
当客户端请求一定数量的ResourceX时,它会生成一个随机UUID,并使用UUID作为requestId将一行添加到Requests表中,然后轮询Logs表以获取requestId。如果success
列为true,则请求成功,否则失败。
具有数据库的服务器为每个资源分配一个线程或进程,例如ProcessX负责ResourceX。 ProcessX从“请求”表中检索所有行,其中success
按时间戳排序,然后从“请求”中删除它们;然后按顺序处理每个请求,为每个成功请求递减内存计数器,并在处理请求结束时更新Resources表上的ResourceX数量。然后,它将每个请求及其resourceType = ResourceX
状态写入Logs表。然后,它再次检索来自success
等请求的请求的所有请求。
使用自动增量整数作为Requests主键,并使ProcessX按主键而不是按时间戳排序可能稍微高效。
一个选项是为每个资源分配一个requestType = RequestX
- 这个线程是唯一访问该资源的数据库表的东西,因此在数据库级别没有锁定。 DAOThread
(例如Web会话)使用并发队列请求资源数量 - 下面的示例使用Java BlockingQueue,但大多数语言都有某种可以使用的并发队列实现。
Worker
Workers将资源请求发送到相应的DAOThread队列; DAOThread按顺序处理这些请求,如果请求的值不超过数量并返回Success,则更新本地资源数量,否则保持数量不变并返回Failure。数据库仅在十次更新后更新,以减少IO的数量; MAX_CHANGES越大,从系统故障中恢复就越复杂。您还可以使用专用的IOThread来执行所有数据库写入 - 这样您就不需要复制任何日志记录或计时(例如,每隔几秒就应该有一个Timer将当前数量刷新到数据库)。
Worker使用SynchronousQueue等待来自DAOThread的响应(SynchronousQueue是一个只能容纳一个项目的BlockingQueue);如果Worker在自己的线程中运行,您可能希望用标准的多项BlockingQueue替换它,以便Worker可以按任何顺序处理ReturnMessages。
有一些数据库,例如Riak具有对计数器的本机支持,因此这可以提高您的IO吞吐量并减少或消除对MAX_CHANGES的需求。
您可以通过引入public class Request {
final int value;
final BlockingQueue<ReturnMessage> queue;
}
public class ReturnMessage {
final int value;
final String resourceType;
final boolean isSuccess;
}
public class DAOThread implements Runnable {
private final int MAX_CHANGES = 10;
private String resourceType;
private int quantity;
private int changeCount = 0;
private DBTable table;
private BlockingQueue<Request> queue;
public DAOThread(DBTable table, BlockingQueue<Request> queue) {
this.table = table;
this.resourceType = table.select("resource_type");
this.quantity = table.select("quantity");
this.queue = queue;
}
public void run() {
while(true) {
Requester request = queue.take();
if(request.value <= quantity) {
quantity -= request.value;
if(++changeCount > MAX_CHANGES) {
changeCount = 0;
table.update("quantity", quantity);
}
request.queue.offer(new ReturnMessage(request.value, resourceType, true));
} else {
request.queue.offer(new ReturnMessage(request.value, resourceType, false));
}
}
}
}
public class Worker {
final Map<String, BlockingQueue<Request>> dbMap;
final SynchronousQueue<ReturnMessage> queue = new SynchronousQueue<>();
public class WorkerThread(Map<String, BlockingQueue<Request>> dbMap) {
this.dbMap = dbMap;
}
public boolean request(String resourceType, int value) {
dbMap.get(resourceType).offer(new Request(value, queue));
return queue.take();
}
}
来缓冲对BufferThread
的请求来进一步提高吞吐量。
DAOThread
工作人员将他们的请求发送到BufferThreads,然后他们等待缓冲public class BufferThread implements Runnable {
final SynchronousQueue<ReturnMessage> returnQueue = new SynchronousQueue<>();
final int BUFFERSIZE = 10;
private DAOThread daoThread;
private BlockingQueue<Request> queue;
private ArrayList<Request> buffer = new ArrayList<>(BUFFERSIZE);
private int tempTotal = 0;
public BufferThread(DAOThread daoThread, BlockingQueue<Request> queue) {
this.daoThread = daoThread;
this.queue = queue;
}
public void run() {
while(true) {
Request request = queue.poll(100, TimeUnit.MILLISECONDS);
if(request != null) {
tempTotal += request.value;
buffer.add(request);
}
if(buffer.size() == BUFFERSIZE || request == null) {
daoThread.queue.offer(new Request(tempTotal, returnQueue));
ReturnMessage message = returnQueue.take();
if(message.isSuccess()) {
for(Request request: buffer) {
request.queue.offer(new ReturnMessage(request.value, daoThread.resourceType, message.isSuccess));
}
} else {
// send unbuffered requests to DAOThread to see if any can be satisfied
for(Request request: buffer) {
daoThread.queue.offer(request);
}
}
buffer.clear();
tempTotal = 0;
}
}
}
}
请求或等待100ms请求通过缓冲区(BUFFERSIZE
),此时他们将缓冲的消息转发给Request request = queue.poll(100, TimeUnit.MILLISECONDS)
。每个DAOThread
可以有多个缓冲区 - 而不是向工作人员发送DAOThread
而是发送Map<String, BlockingQueue<Request>>
,每个Map<String, ArrayList<BlockingQueue<Request>>>
一个队列,工作人员使用计数器或随机数生成器,以确定发送请求的BufferThread
。请注意,如果BufferThread
太大和/或如果BUFFERSIZE
太多,那么当等待缓冲区填满时,工作人员会遇到很长的暂停时间。