假设我有一个应用程序,其中用户必须输入刮刮卡代码并将1000美元转移到他的帐户!
现在我的应用程序正在侦听代码。这样的事情会发生 -
def RedeemCode(code, user_id):
isvalid = checkValidCode(code) # queries database
if isvalid is True:
setCodeAsExpired(code) # updates database
addMoneyToAccount(amount=1000, id=user_id)
假设2个请求在同一时间使用相同的code
和user_id
。同样假设checkValidCode
为它们返回True
,因为执行顺序为:
checkValidCode[request1] -> checkValidCode[request2] -> setCodeAsExpired[request1] -> setCodeAsExpired[request2]
通过这种方法,存在两次贷记的问题。
在这种情况下,系统如何设计以防止此类问题? (对不起,如果这是一个微不足道的问题。我正在考虑这个系统如何工作并且在此时陷入困境)
答案 0 :(得分:1)
这是您想要锁和事务设计(原子)的地方。您需要检查有效性,使代码到期以及将用户记入单个原子,不可分割的事务的过程。
但是,您创建此锁定的,不可分割的事务因语言/库/框架而异。
由于您展示的代码是Python,因此甚至不可能同时执行它(请参阅Python的全局解释器锁)。
在Java中,你有一个synchronized
关键字用于锁定整个对象的方法,因此它不仅可以防止任何其他线程同时执行该特定方法的代码,而且还可以防止它将它们锁定在调用同一对象中的任何其他同步方法之外。
在C#中,您可以管理自己的资源锁,但不需要单独的特定于锁的对象类型。有lock
关键字的语言级支持,但它比Java设计更精细。这既好又坏:能够以更多的自由裁量权来锁定事物对提高效率是有益的,但它可能对安全性有害,而且更容易出错。
在更多的"卷起袖子,弄脏" 硬件级语言,如C和C ++,你经常将关键部分作为单独的对象类型处理根据您选择的粒度级别锁定适当的代码段,而不仅仅是整个对象。在这些情况下,甚至可以达到比较和交换(CAS)互锁原子操作的水平,以确保甚至像递增整数这样的东西是原子事务(它实际上不一定是硬件级别的原子,尝试增加相同整数的两个线程实际上可能导致竞争条件。)
请注意,锁定可能非常昂贵,并且在最坏的情况下,有效地将系统性能转换为比单线程更差的性能,因为在等待访问a时线程数被挂起高度争用的共享资源。它可以在他们遇到瓶颈并等待轮到他们的情况下创建线程交通拥堵。
因此,有很多策略可以优化这一点,包括无锁设计(有时甚至是无等待)。有时也可以分割共享资源。
例如,您的客户数据库可能会拆分为单独的块,如果来自同一客户的两个请求进入,则只需要锁定。这样您就可以将一个可能很大的共享资源转换为可以单独锁定的许多资源。 / p>
你似乎想知道锁是否会超时。有些设计允许它,但如果它们这样做,它们总是会超时而忽略执行关键部分(基本上是在中止尝试的方面犯错)。如果一个线程可以超时并且同时执行临界区,那么它将破坏整个锁定点。所以当你锁定某个没有两个线程同时执行该代码的东西时,你就有了很强的保证。
您想要研究的是线程锁,它们如何工作以及事务编码和思考的一般心态。这对于异常和错误处理也很重要,以确保在关键操作过程中遇到错误时,它不会使系统处于半完成状态。无效的状态。事务应该作为一个整体成功或作为一个整体中止/回滚。
答案 1 :(得分:0)
当数据库是应用程序的真实来源时,在数据库级别使用约束强制执行数据完整性非常重要。正如我已经表明的那样,即使最终结果一旦持久无效,也会有一些情况会通过验证。在最糟糕的情况下,这是一个严重的安全漏洞,即使在最好的情况下,也会导致数据完整性问题。简单的解决方案是在模型的外键和优惠券代码字段的组合上添加唯一索引,这样数据库服务器就会拒绝重复项,因为现代数据库服务器既是原子的又是线程安全的。