如何获得对某些会话条目的独占访问权?

How to get exclusive access to some session entry?

由于 REST 服务的远程调用特性,它们经常处于 运行 相互竞争的状态。每天要争夺的资源之一是会话。为了实用,您需要能够在流程开始时锁定资源,并在完成时将其解除。

现在我的问题是,Spring 会话是否有任何功能来处理会话条目的竞争条件?

或 Java 中的任何其他库/框架!!!

如果您使用 Spring 控制器,则可以使用

RequestMappingHandlerAdapter.setSynchronizeOnSession-boolean-

这将使每个控制器方法在存在会话时同步。

HttpSession.setAttribute 是线程安全的。但是 getAttribute 后跟 setAttribute 必须手动设置胎面安全。

synchronized(session) {
    session.setAttribute("foo", "bar");
    session.getAttribute("foo");
}

在 spring 会话 bean 的情况下也可以这样做。

synchronized(session) {
    //do something with the session bean
}

#编辑

如果多个容器具有正常的 spring 会话 bean,您将不得不使用 sticky sessions。这将确保一个会话状态存储在一个容器中,并且每次请求同一会话时都会访问该容器。这必须借助 BigIP cookies 之类的东西在负载均衡器上完成。 Rest 的工作方式与存在单个容器的单个会话相同,因此锁定会话就足够了。

如果您想跨实例使用会话共享,可以在容器上提供支持,例如 Tomcat and Jetty

这些方法使用后端数据库或其他一些持久性机制来存储状态。

出于同样的目的,您可以尝试使用 Spring Session。使用 Redis 配置是微不足道的。由于 Redis 是单线程的,它确保以原子方式访问条目的一个实例。

以上方法是非侵入性的。基于数据库和 Redis 的方法都支持 transactions.

但是,如果您想更好地控制分布式状态和锁定,您可以尝试使用分布式数据网格,如 Hazelcast 和 Gemfire。

我亲自使用过 Hazelcast,它确实提供了 methods to lock entries made in the map

#Edit2

尽管我认为使用 Spring Session 和 Redis 处理事务应该足够了,以确保您需要分布式锁定。必须从 Redis 本身获取 Lock 对象。由于 Redis 是单线程的,个人实现也可以通过使用类似 INCR

算法会像下面这样

//lock_num is the semaphore/lock object

lock_count = INCR lock_num
while(true) {
    if(lock_count != 1) {
        DECR lock_num
    } else {
        break
    }
    wait(wait_time_period)
}

//do processing in critical section

DECR lock_num

不过,值得庆幸的是 Spring 已经通过 RedisLockRegistry. More documentation on usage is here.

提供了这种分布式锁实现

如果您决定在没有 spring 的情况下使用普通 Jedis,那么这里是 Jedis 的分布式锁:Jedis Lock.

//from https://github.com/abelaska/jedis-lock
Jedis jedis = new Jedis("localhost");
JedisLock lock = new JedisLock(jedis, "lockname", 10000, 30000);
lock.acquire();
try {
  // do some stuff
}
finally {
  lock.release();
}

这两个都应该像 Hazelcast 锁定一样工作。

正如之前的回答所述。如果您正在使用 Spring Session 并且您担心并发访问 Session 时的线程安全,您应该设置:

RequestMappingHandlerAdapter.setSynchronizeOnSession(true);

可以在此处找到一个示例 EnableSynchronizeOnSessionPostProcessor :

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

public class EnableSynchronizeOnSessionPostProcessor implements BeanPostProcessor {
    private static final Logger logger = LoggerFactory
        .getLogger(EnableSynchronizeOnSessionPostProcessor.class);

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // NO-OP
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof RequestMappingHandlerAdapter) {
            RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
            logger.info("enable synchronizeOnSession => {}", adapter);
            adapter.setSynchronizeOnSession(true);
        }
        return bean;
    }
}

Sticky Sessions and Session Replication

关于集群应用程序和会话,SO 上有一个非常好的 post,讨论了这个主题:Sticky Sessions and Session Replication

根据我的经验,您可能需要 Sticky Session 和 Session 复制。 您使用粘性会话来消除跨节点的并发会话访问,因为粘性会话会将会话固定到单个节点,并且同一会话的每个后续请求将始终定向到该节点。这消除了跨节点会话访问问题。

复制会话主要在节点出现故障时有用。通过复制会话,当一个节点出现故障时,未来对现有会话的请求将被定向到另一个节点,该节点将拥有原始会话的副本,并使故障转移对用户透明。

有很多框架支持会话复制。我用于大型项目的是开源 Hazelcast.

回应您在@11thdimension post 上发表的评论:

我认为您处在一个具有挑战性的领域。基本上,您希望强制所有会话操作在集群中的节点之间是原子的。这使我倾向于跨节点的公共会话存储,其中访问是同步的(或类似的东西)。

多会话存储/复制框架肯定支持外部存储概念,我相信 Reddis 支持。我最熟悉 Hazelcast 并将以它为例。

Hazelcast 允许配置会话持久性以使用公共数据库。 如果您查看 Map Persistence 部分,它会显示示例和选项说明。

概念说明:

Hazelcast allows you to load and store the distributed map entries from/to a persistent data store such as a relational database. To do this, you can use Hazelcast's MapStore and MapLoader interfaces.

Data store needs to be a centralized system that is accessible from all Hazelcast Nodes. Persistence to local file system is not supporte

Hazelcast supports read-through, write-through, and write-behind persistence modes which are explained in below subsections.

有趣的模式是直写:

Write-Through

MapStore can be configured to be write-through by setting the write-delay-seconds property to 0. This means the entries will be put to the data store synchronously.

In this mode, when the map.put(key,value) call returns:

MapStore.store(key,value) is successfully called so the entry is persisted. In-Memory entry is updated. In-Memory backup copies are successfully created on other JVMs (if backup-count is greater than 0). The same behavior goes for a map.remove(key) call. The only difference is that MapStore.delete(key) is called when the entry will be deleted.

我认为,使用这个概念,加上正确设置商店的数据库表以锁定 insert/update/deletes 上的条目,您可以完成您想要的。

祝你好运!