简体   繁体   English

启用 ASP.Net Core Session 锁定?

[英]Enable ASP.Net Core Session Locking?

According to the ASP.Net Core docs , the behaviour of the session state has changed in that it is now non-locking:根据 ASP.Net Core docs , session state 的行为已经改变,因为它现在是非锁定的:

Session state is non-locking. Session state 是非锁定的。 If two requests simultaneously attempt to modify the contents of a session, the last request overrides the first.如果两个请求同时尝试修改 session 的内容,则最后一个请求将覆盖第一个请求。 Session is implemented as a coherent session, which means that all the contents are stored together. Session 实现为连贯的 session,这意味着所有内容都存储在一起。 When two requests seek to modify different session values, the last request may override session changes made by the first.当两个请求试图修改不同的 session 值时,最后一个请求可能会覆盖第一个请求所做的 session 更改。

My understanding is that this is different to the behaviour of the session in the.Net Framework, where the user's session was locked per request so that whenever you read from/wrote to it, you weren't overwriting another request's data or reading stale data, for that user.我的理解是,这与 .Net Framework 中 session 的行为不同,其中用户的 session 每个请求都被锁定,因此无论何时读取/写入它,都不会覆盖另一个请求的数据或读取过时的数据, 对于该用户。

My question(s):我的问题:

  1. Is there a way to re-enable this per-request locking of the user's session in.Net Core?有没有办法重新启用用户的 session in.Net Core 的每个请求锁定?

  2. If not, is there a reliable way to use the session to prevent duplicate submission of data for a given user?如果没有,是否有可靠的方法使用 session 来防止给定用户重复提交数据? To give a specific example, we have a payment process that involves the user returning from an externally hosted ThreeDSecure (3DS) iFrame (payment card security process).举一个具体的例子,我们有一个支付流程,涉及用户从外部托管的 ThreeDSecure (3DS) iFrame(支付卡安全流程)返回。 We are noticing that sometimes (somehow) the user is submitting the form within the iFrame multiple times, which we have no control over.我们注意到有时(不知何故)用户在 iFrame 中多次提交表单,这是我们无法控制的。 As a result this triggers multiple callbacks to our application.因此,这会触发对我们应用程序的多个回调。 In our previous.Net Framework app, we used the session to indicate if a payment was in progress.在我们之前的 .Net Framework 应用程序中,我们使用 session 来指示付款是否正在进行中。 If this flag was set in the session and you hit the 3DS callback again, the app would stop you proceeding.如果在 session 中设置了此标志并且您再次点击 3DS 回调,应用程序将阻止您继续。 However, now it seems that because the session isn't locked, when these near simultaneous, duplicate callbacks occur, thread 'A' sets 'payment in progress = true' but thread 'B' doesn't see that in time, it's snapshot of the session is still seeing 'payment in progress = false' and the callback logic is processed twice.然而,现在看来,因为 session 没有被锁定,当这些几乎同时发生的重复回调发生时,线程“A”设置“付款进行中 = true”但线程“B”没有及时看到,它是快照session 的 'payment in progress = false' 并且回调逻辑被处理了两次。

What are some good approaches to handling simultaneous requests accessing the same session, now that the way the session works has changed?既然 session 的工作方式发生了变化,那么处理访问同一 session 的同时请求有哪些好的方法?

The problem that you have faced with is called Race Condition ( stackoverflow , wiki ).您面临的问题称为竞争条件( stackoverflowwiki )。 To cut-through, you'd like to get exclusive access to the session state, you can achieve that in several ways and they highly depend on your architecture.要突破,您希望获得对 session state 的独家访问权,您可以通过多种方式实现这一目标,它们高度依赖于您的架构。

In-process synchronization进程内同步

If you have a single machine with a single process handling all requests (for example you use a self-hosted server, Kestrel), you may use lock .如果您有一台机器和一个进程处理所有请求(例如,您使用自托管服务器 Kestrel),则可以使用lock Just do it correctly and not how @TMG suggested.只要正确地做,而不是@TMG 建议的方式。

Here is an implementation reference:这是一个实现参考:

  1. Use single global object to lock all threads:使用单个全局 object 锁定所有线程:
  private static object s_locker = new object();

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

Pros: a simple solution Cons: all requests from all users will wait on this lock优点:一个简单的解决方案缺点:所有用户的所有请求都将等待这个锁

  1. use per-session lock object.使用每会话锁定 object。 Idea is similar, but instead of a single object you just use a dictionary:想法是相似的,但是您只需使用字典而不是单个 object:
    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;
    }
}

Pros: manages concurrency on the session level Cons: more complex solution优点:管理 session 级别的并发 缺点:更复杂的解决方案

Remember that lock-based option will work only when the same process on the webserver handling all user's requests - lock is intra-process synchronization mechanism, Depending on what you use as a persistent layer for sessions (like NCache or Redis).请记住,基于锁的选项仅在处理所有用户请求的网络服务器上的同一进程时才有效 - 锁是进程内同步机制,具体取决于您用作会话的持久层(如 NCache 或 Redis)。 this option might be the most performant though.不过,此选项可能是性能最高的。

Cross-process synchronization跨进程同步

If there are several processes on the machine (for example you have IIS and apppool is configured to run multiple worker processes), then you need to use kernel-level synchronization primitive, like Mutex .如果机器上有多个进程(例如你有 IIS 并且 apppool 被配置为运行多个工作进程),那么你需要使用内核级同步原语,比如Mutex

Cross-machine synchronization跨机同步

If you have a load balancer (LB) in front of your webfarm so that any of N machines can handle user's request, then getting exclusive access is not so trivial.如果您的 webfarm 前面有一个负载均衡器 (LB),以便 N 台机器中的任何一台都可以处理用户的请求,那么获得独占访问权限就不是那么简单了。

One option here is to simplify the problem by enabling the ' sticky session ' option in your LB so that all requests from the same user (session) will be routed to the same machine.这里的一个选项是通过启用 LB 中的“ sticky session ”选项来简化问题,以便将来自同一用户(会话)的所有请求路由到同一台机器。 In this case, you are fine to use any cross-process or in-process synchronization option (depends on what you have running there).在这种情况下,您可以使用任何跨进程或进程内同步选项(取决于您在那里运行的内容)。

Another option is to externalize synchronization, for example, move it to the transactional DB, something similar to what @HoomanBahreini suggested.另一种选择是将同步外部化,例如,将其移动到事务数据库,类似于@HoomanBahreini 的建议。 Beware that you need to be very cautious on handling failure scenarios: you may mark your session as in progress and then your webserver which handled it crashed leaving it locked in DB.请注意,您在处理故障情况时需要非常谨慎:您可以将 session 标记为正在进行中,然后处理它的网络服务器崩溃并将其锁定在数据库中。

Important重要的

In all of these options you must ensure that you obtain lock before reading the state and hold it until you update the state.在所有这些选项中,您必须确保在读取 state之前获得锁定并持有它直到您更新 state。

Please clarify what option is the closest to your case and I can provide more technical details.请说明最接近您的情况的选项,我可以提供更多技术细节。

Session is designed to store temporary user data among multiple requests, a good example is login-state... without session you would have to login to stackoverflow.com every time you open a new question... but the website remembers you, because your send them your session state inside a cookie. Session 旨在在多个请求中存储临时用户数据,一个很好的例子是登录状态......没有 session 你将不得不登录到stackoverflow.com因为每次打开一个新问题网站都会记住你......您将您的 session state 发送到 cookie 中。 According to Microsoft :根据微软

The session data is backed by a cache and considered ephemeral data. session 数据由缓存支持并被视为临时数据。 The site should continue to function without the session data.该站点应继续 function 没有 session 数据。 Critical application data should be stored in the user database and cached in session only as a performance optimization.关键应用程序数据应存储在用户数据库中并缓存在 session 中,仅作为性能优化。

It is quite simple to implement a locking mechanism to solve your mutex issue, however the session itself is not a reliable storage and you may loose its content at any time.实施锁定机制来解决互斥锁问题非常简单,但是 session 本身不是可靠的存储,您可能随时丢失其内容。

How to identify duplicate payments?如何识别重复付款?

The problem is you are getting multiple payment requests and you want to discard the duplicate ones... what's your definition of a duplicate payment?问题是您收到多个付款请求,并且您想丢弃重复的付款……您对重复付款的定义是什么?

Your current solution discard the second payment while a first one is in progress... let's say your payment takes 2 seconds to complete... what will happen if you receive the duplicate payment after 3 seconds?您当前的解决方案在第一次付款正在进行时放弃第二次付款...假设您的付款需要 2 秒才能完成...如果您在 3 秒后收到重复付款会发生什么?

Every reliable payment system includes a unique PaymentId in their request... what you need to do is to mark this PaymentId as processed in your DB.每个可靠的支付系统在他们的请求中都包含一个唯一的PaymentId ......您需要做的是将此PaymentId标记为在您的数据库中处理。 This way you won't process the same payment twice, no matter when the duplicate request arrives.这样,无论重复请求何时到达,您都不会两次处理相同的付款。

You can use a Unique Constraint on PaymentId to prevent duplicate payments:您可以在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;
}

Same example using lock :使用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;
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM