启用 ASP.Net Core 会话锁定?

Posted

技术标签:

【中文标题】启用 ASP.Net Core 会话锁定?【英文标题】:Enable ASP.Net Core Session Locking? 【发布时间】:2020-10-15 04:43:03 【问题描述】:

根据 ASP.Net Core docs,会话状态的行为已经改变,现在它是非锁定的:

会话状态是非锁定的。如果两个请求同时尝试修改会话的内容,则最后一个请求会覆盖第一个请求。 Session 被实现为一个连贯的会话,这意味着所有的内容都存储在一起。当两个请求试图修改不同的会话值时,最后一个请求可能会覆盖第一个请求所做的会话更改。

我的理解是,这与 .Net 框架中会话的行为不同,其中用户的会话被锁定每个请求,因此无论何时读取/写入它,您都不会覆盖另一个请求的数据或为该用户读取过时的数据。

我的问题:

    有没有办法在 .Net Core 中重新启用用户会话的按请求锁定?

    如果没有,是否有可靠的方法使用会话来防止给定用户重复提交数据?举一个具体的例子,我们有一个支付流程,涉及用户从外部托管的 ThreeDSecure (3DS) iFrame(支付卡安全流程)返回。我们注意到有时(不知何故)用户在 iFrame 中多次提交表单,我们无法控制。因此,这会触发对我们应用程序的多个回调。在我们之前的 .Net Framework 应用程序中,我们使用会话来指示付款是否正在进行中。如果在会话中设置了此标志并且您再次点击 3DS 回调,则应用程序将阻止您继续进行。然而,现在似乎因为会话没有被锁定,当这些几乎同时发生的重复回调发生时,线程'A'设置'payment in progress = true'但线程'B'没有及时看到,它是快照的会话仍然看到'payment in progress = false'并且回调逻辑被处理了两次。

既然会话的工作方式发生了变化,那么处理访问同一会话的同时请求有哪些好的方法?

【问题讨论】:

不要使用会话来存储支付状态等重要的东西,使用持久且易于访问的数据库。会话可能会在多种情况下丢失,这是您在涉及金钱时不想要的东西......此外,使用唯一索引可以避免那些双重交易,我相信您可以为每次付款创建一个唯一密钥(用户 ID + 交易 ID、用户 ID + 产品 ID 或其他),因此您可以使用具有唯一索引的密钥来避免重新发出相同的付款。 它允许多次读取和写入...只是最后一次写入是赢家,这意味着...在读取时锁定下一次读取之前...即使它不会改变。基于此......并且只有 1 个线程应该更新值......是什么阻止你阅读它,直到你拥有你想要的值。 aka 需要更多地了解您的流程。至于什么是回调以及为什么......就像它通常如何工作......在改变之前......它只是等待。如果是这样,为什么不让调用循环,直到会话值是您要查找的值.. 不推荐 We are noticing that sometimes (somehow) the user is submitting the form within the iFrame multiple times 阻止他们这样做? 这是在 3DS 过程中从 iframe 返回时 - 我们无法访问/控制 iframe 中的表单如何提交。 we used the session to indicate if a payment was in progress 你仍然可以这样做..检查值.. 【参考方案1】:

您遇到的问题称为竞争条件(***、wiki)。要实现直通,您希望获得对会话状态的独占访问权,您可以通过多种方式实现这一目标,它们高度依赖于您的架构。

进程内同步

如果您有一台机器和一个进程处理所有请求(例如,您使用自托管服务器 Kestrel),您可以使用lock。正确地做,而不是按照@TMG 的建议去做。

这是一个实现参考:

    使用单个全局对象锁定所有线程:
  private static object s_locker = new object();

  public bool Process(string transaction) 
      lock (s_locker) 
        if(!HttpContext.Session.TryGetValue("TransactionId", out _)) 
           ... handle transaction
        
      
  

优点:简单的解决方案 缺点:所有用户的所有请求都会在这个锁上等待

    使用每会话锁定对象。想法是相似的,但你只使用字典而不是单个对象:
    internal class LockTracker : IDisposable
    
        private static Dictionary<string, LockTracker> _locks = new Dictionary<string, LockTracker>();
        private int _activeUses = 0;
        private readonly string _id;

        private LockTracker(string id) => _id = id;

        public static LockTracker Get(string id)
        
            lock(_locks)
            
                if(!_locks.ContainsKey(id))
                    _locks.Add(id, new LockTracker(id));
                var res = _locks[id];
                res._activeUses += 1;
                return res;
            
        

        void IDisposable.Dispose()
        
            lock(_locks)
            
                _activeUses--;
                if(_activeUses == 0)
                    _locks.Remove(_id);
            
        
    


public bool Process(string transaction)

    var session = HttpContext.Session;
    var locker = LockTracker.Get(session.Id);
    using(locker) // remove object after execution if no other sessions use it
    lock (locker) // synchronize threads on session specific object
    
        // check if current session has already transaction in progress
        var transactionInProgress = session.TryGetValue("TransactionId", out _);
        if (!transactionInProgress)
        
            // if there is no transaction, set and handle it
            HttpContext.Session.Set("TransactionId", System.Text.Encoding.UTF8.GetBytes(transaction));
            HttpContext.Session.Set("StartTransaction", BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
            // handle transaction here
        
        // return whatever you need, here is just a boolean.
        return transactionInProgress;
    

优点:在会话级别管理并发 缺点:更复杂的解决方案

请记住,基于锁的选项只有在网络服务器上的同一进程处理所有用户的请求时才有效 - 锁是进程内同步机制!根据您用作会话的持久层(如 NCache 或 Redis),此选项可能是性能最高的。

跨进程同步

如果机器上有多个进程(例如你有 IIS 并且 apppool 被配置为运行多个工作进程),那么你需要使用内核级同步原语,比如Mutex。

跨机同步

如果您的 webfarm 前面有一个负载平衡器 (LB),以便 N 台机器中的任何一台都可以处理用户的请求,那么获得独占访问权限就不是那么容易了。

这里的一个选项是通过启用 LB 中的“sticky session”选项来简化问题,以便将来自同一用户(会话)的所有请求路由到同一台机器。在这种情况下,您可以使用任何跨进程或进程内同步选项(取决于您在那里运行的内容)。

另一种选择是将同步外部化,例如,将其移至事务数据库,类似于@HoomanBahreini 建议的内容。请注意,您在处理失败场景时需要非常谨慎:您可能会将会话标记为正在进行,然后处理它的网络服务器崩溃并将其锁定在数据库中。

重要

在所有这些选项中,您必须确保读取状态并持有之前获得锁定直到你更新状态。

请说明最接近您的情况的选项,我可以提供更多技术细节。

【讨论】:

感谢您的建议。我们目前正在使用粘性会话(不理想)。我目前正在研究的方法是使用数据库记录是否正在进行付款,利用数据库级别的锁我可以防止同一会话的多个线程同时将“付款进行中”设置为真时间... 您能详细说明一下吗? '你可以使用锁。只是正确地做,而不是@TMG 建议的方式。有兴趣了解如何锁定每个用户的会话(在代码中) 您的第二个解决方案不是线程安全的,因为在另一个线程已经检索到对象并等待锁定之后,线程可能会从字典中删除条目。然后,第三个线程将接收不同的锁定对象,并且两个线程可以进入锁定部分。对吗? @dr0n3 你是完全正确的,改变了消除种族的解决方案。【参考方案2】:

会话旨在存储多个请求之间的临时用户数据,一个很好的例子是登录状态...没有会话,您每次打开新问题时都必须登录***.com ...但是该网站会记住您,因为您将会话状态发送到 cookie 中。根据Microsoft:

会话数据由缓存支持并被视为临时数据。 该站点应在没有会话数据的情况下继续运行。 关键应用程序数据应存储在用户数据库中,并且 缓存在会话中仅作为性能优化。

实现一个锁定机制来解决您的互斥锁问题非常简单,但是会话本身并不是一个可靠的存储,您可能随时丢失它的内容。

如何识别重复付款?

问题是您收到多个付款请求,并且您想丢弃重复的付款...您对重复付款的定义是什么?

您当前的解决方案在第一笔付款正在进行时放弃第二笔付款...假设您的付款需要 2 秒才能完成...如果您在 3 秒后收到重复付款会怎样?

每个可靠的支付系统在他们的请求中都包含一个唯一的PaymentId...您需要做的是在您的数据库中将此PaymentId 标记为已处理。这样,无论重复请求何时到达,您都不会处理两次相同的付款。

您可以在PaymentId 上使用Unique Constraint 来防止重复付款:

public bool ProcessPayment(Payment payment) 
    bool res = InsertIntoDb(payment);
    if (res == false) 
        return false; // <-- insert has failed because PaymentId is not unique
            
    
    Process(payment);
    return true;

使用lock 的相同示例:

public class SafePayment 
    private static readonly Object lockObject = new Object();

    public bool ProcessPayment(Payment payment) 
        lock (lockObject) 
            var duplicatePayment = ReadFromDb(payment.Id);
            if (duplicatePayment != null) 
                return false; // <-- duplicate 
            
            
            Process(payment);
            WriteToDb(payment);
            return true;
        
    

【讨论】:

您的 DB 示例根本没有消除竞争条件:2 个线程可以从 DB 读取状态,看到 PaymentInProgress 为假并继续处理:第一个线程将持有锁并执行逻辑,第二个线程将等待用于日志和!执行逻辑! .您应该使用双重检查锁定 (en.wikipedia.org/wiki/Double-checked_locking#Usage_in_C%23) 或消除数据库中的竞争,例如使用并发令牌 (docs.microsoft.com/en-us/ef/core/modeling/…)。 为每个请求锁定同一个对象将只允许一次处理一笔付款,这对于繁忙的电子商务网站显然是不可接受的! 我的问题特别提到“用户会话的每个请求锁定” - 这是锁定的一个示例,但您的示例不会只锁定它所属请求的会话,它会锁定访问所有请求的会话并强制它们一次执行一个。

以上是关于启用 ASP.Net Core 会话锁定?的主要内容,如果未能解决你的问题,请参考以下文章

非锁定进程内 ASP.NET 会话状态存储

ASP.NET MVC:如何防止会话锁定?

完全替换 ASP.Net 的会话

ASP.NET Core 会话超时

ASP.Net Core Sql 会话持久化

如何在 ASP.NET Core 中启用 CORS