热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

服务注册发现consul之四:分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

一、基于keyvalue实现我们在构建分布式系统的时候,经常需要控制对共享资源的互斥访问。这个时候我们就涉及到分布式锁(也称为全局锁)的实现,基于目前的各种工具,我们已经有了大量的

一、基于key/value实现

我们在构建分布式系统的时候,经常需要控制对共享资源的互斥访问。这个时候我们就涉及到分布式锁(也称为全局锁)的实现,基于目前的各种工具,我们已经有了大量的实现方式,比如:基于Redis的实现、基于Zookeeper的实现。本文将介绍一种基于Consul 的Key/Value存储来实现分布式锁以及信号量的方法。

分布式锁实现

基于Consul的分布式锁主要利用Key/Value存储API中的acquire和release操作来实现。acquire和release操作是类似Check-And-Set的操作:

 

- acquire操作只有当锁不存在持有者时才会返回true,并且set设置的Value值,同时执行操作的session会持有对该Key的锁,否则就返回false

 

- release操作则是使用指定的session来释放某个Key的锁,如果指定的session无效,那么会返回false,否则就会set设置Value值,并返回true

 

具体实现中主要使用了这几个Key/Value的API:

- create session:https://www.consul.io/api/session.html#session_create

- delete session:https://www.consul.io/api/session.html#delete-session

- KV acquire/release:https://www.consul.io/api/kv.html#create-update-key

 

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

基本流程

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

 

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

具体实现

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

public class Lock {

 

    private static final String prefix = "lock/";  // 同步锁参数前缀

 

    private ConsulClient consulClient;

    private String sessionName;

    private String sessiOnId= null;

    private String lockKey;

 

    /**

     *

     * @param consulClient

     * @param sessionName   同步锁的session名称

     * @param lockKey       同步锁在consul的KV存储中的Key路径,会自动增加prefix前缀,方便归类查询

     */

    public Lock(ConsulClient consulClient, String sessionName, String lockKey) {

        this.cOnsulClient= consulClient;

        this.sessiOnName= sessionName;

        this.lockKey = prefix + lockKey;

    }

 

    /**

     * 获取同步锁

     *

     * @param block     是否阻塞,直到获取到锁为止

     * @return

     */

    public Boolean lock(boolean block) {

        if (sessionId != null) {

            throw new RuntimeException(sessionId + " - Already locked!");

        }

        sessiOnId= createSession(sessionName);

        while(true) {

            PutParams putParams = new PutParams();

            putParams.setAcquireSession(sessionId);

            if(consulClient.setKVValue(lockKey, "lock:" + LocalDateTime.now(), putParams).getValue()) {

                return true;

            } else if(block) {

                continue;

            } else {

                return false;

            }

        }

    }

 

    /**

     * 释放同步锁

     *

     * @return

     */

    public Boolean unlock() {

        PutParams putParams = new PutParams();

        putParams.setReleaseSession(sessionId);

        boolean result = consulClient.setKVValue(lockKey, "unlock:" + LocalDateTime.now(), putParams).getValue();

        consulClient.sessionDestroy(sessionId, null);

        return result;

    }

 

    /**

     * 创建session

     * @param sessionName

     * @return

     */

    private String createSession(String sessionName) {

        NewSession newSession = new NewSession();

        newSession.setName(sessionName);

        return consulClient.sessionCreate(newSession, null).getValue();

    }

 

}

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

单元测试

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

下面单元测试的逻辑:通过线程的方式来模拟不同的分布式服务来竞争锁。多个处理线程同时以阻塞方式来申请分布式锁,当处理线程获得锁之后,Sleep一段随机事件,以模拟处理业务逻辑,处理完毕之后释放锁。

 

public class TestLock {

 

    private Logger logger = Logger.getLogger(getClass());

 

    @Test

    public void testLock() throws Exception  {

        new Thread(new LockRunner(1)).start();

        new Thread(new LockRunner(2)).start();

        new Thread(new LockRunner(3)).start();

        new Thread(new LockRunner(4)).start();

        new Thread(new LockRunner(5)).start();

        Thread.sleep(200000L);

    }

  

    class LockRunner implements Runnable {

 

        private Logger logger = Logger.getLogger(getClass());

        private int flag;

 

        public LockRunner(int flag) {

            this.flag = flag;

        }

 

        @Override

        public void run() {

            Lock lock = new Lock(new ConsulClient(), "lock-session", "lock-key");

            try {

                if (lock.lock(true)) {

                    logger.info("Thread " + flag + " start!");

                    Thread.sleep(new Random().nextInt(3000L));

                    logger.info("Thread " + flag + " end!");

                }

            } catch (Exception e) {

                e.printStackTrace();

            } finally {

                lock.unlock();

            }

        }

    }  

}

 

单元测试执行结果如下:

 

2017-04-12 21:28:09,698 INFO  [Thread-0] LockRunner - Thread 1 start!

2017-04-12 21:28:12,717 INFO  [Thread-0] LockRunner - Thread 1 end!

2017-04-12 21:28:13,219 INFO  [Thread-2] LockRunner - Thread 3 start!

2017-04-12 21:28:15,672 INFO  [Thread-2] LockRunner - Thread 3 end!

2017-04-12 21:28:15,735 INFO  [Thread-1] LockRunner - Thread 2 start!

2017-04-12 21:28:17,788 INFO  [Thread-1] LockRunner - Thread 2 end!

2017-04-12 21:28:18,249 INFO  [Thread-4] LockRunner - Thread 5 start!

2017-04-12 21:28:19,573 INFO  [Thread-4] LockRunner - Thread 5 end!

2017-04-12 21:28:19,757 INFO  [Thread-3] LockRunner - Thread 4 start!

2017-04-12 21:28:21,353 INFO  [Thread-3] LockRunner - Thread 4 end!

 

从测试结果我们可以看到,通过分布式锁的形式来控制并发时,多个同步操作只会有一个操作能够被执行,其他操作只有在等锁释放之后才有机会去执行,所以通过这样的分布式锁,我们可以控制共享资源同时只能被一个操作进行执行,以保障数据处理时的分布式并发问题。

 

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

优化建议

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

本文我们实现了基于Consul的简单分布式锁,但是在实际运行时,可能会因为各种各样的意外情况导致unlock操作没有得到正确地执行,从而使得分布式锁无法释放。所以为了更完善的使用分布式锁,我们还必须实现对锁的超时清理等控制,保证即使出现了未正常解锁的情况下也能自动修复,以提升系统的健壮性。那么如何实现呢?请持续关注我的后续分解!

 

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

参考文档

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

Key/Value的API:https://www.consul.io/api/kv.html

 

二、基于consul分布式信号量实现

在上面《基于Consul的分布式锁实现》中我们介绍如何基于Consul的KV存储来实现分布式互斥锁。本文将继续讨论基于Consul的分布式锁实现。信号量是我们在实现并发控制时会经常使用的手段,主要用来限制同时并发线程或进程的数量,比如:Zuul默认情况下就使用信号量来限制每个路由的并发数,以实现不同路由间的资源隔离。

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端,确认这些信号量VI引用的是初始创建的信号量。如在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

实现思路

- 信号量存储:semaphore/key

- acquired操作:

    - 创建session

    - 锁定key竞争者:semaphore/key/session

    - 查询信号量:semaphore/key/.lock,可以获得如下内容(如果是第一次创建信号量,将获取不到,这个时候就直接创建)

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

 

- 如果持有者已达上限,返回false,如果阻塞模式,就继续尝试acquired操作

- 如果持有者未达上限,更新semaphore/key/.lock的内容,将当前线程的sessionId加入到holders中。注意:更新的时候需要设置cas,它的值是“查询信号量”步骤获得的“ModifyIndex”值,该值用于保证更新操作的基础没有被其他竞争者更新。如果更新成功,就开始执行具体逻辑。如果没有更新成功,说明有其他竞争者抢占了资源,返回false,阻塞模式下继续尝试acquired操作

- release操作:

    - 从semaphore/key/.lock的holders中移除当前sessionId

    - 删除semaphore/key/session

    - 删除当前的session

 

流程图

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁

 

代码实现

public class Semaphore {

 

    private Logger logger = Logger.getLogger(getClass());

 

    private static final String prefix = "semaphore/";  // 信号量参数前缀

 

    private ConsulClient consulClient;

    private int limit;

    private String keyPath;

    private String sessiOnId= null;

    private boolean acquired = false;

 

    /**

     *

     * @param consulClient consul客户端实例

     * @param limit 信号量上限值

     * @param keyPath 信号量在consul中存储的参数路径

     */

    public Semaphore(ConsulClient consulClient, int limit, String keyPath) {

        this.cOnsulClient= consulClient;

        this.limit = limit;

        this.keyPath = prefix + keyPath;

    }

 

    /**

     * acquired信号量

     *

     * @param block 是否阻塞。如果为true,那么一直尝试,直到获取到该资源为止。

     * @return

     * @throws IOException

     */

    public Boolean acquired(boolean block) throws IOException {

 

        if(acquired) {

            logger.error(sessionId + " - Already acquired");

            throw new RuntimeException(sessionId + " - Already acquired");

        }

 

        // create session

        clearSession();

        this.sessiOnId= createSessionId("semaphore");

        logger.debug("Create session : " + sessionId);

 

        // add contender entry

        String cOntenderKey= keyPath + "/" + sessionId;

        logger.debug("contenderKey : " + contenderKey);

        PutParams putParams = new PutParams();

        putParams.setAcquireSession(sessionId);

        Boolean b = consulClient.setKVValue(contenderKey, "", putParams).getValue();

        if(!b) {

            logger.error("Failed to add contender entry : " + contenderKey + ", " + sessionId);

            throw new RuntimeException("Failed to add contender entry : " + contenderKey + ", " + sessionId);

        }

 

        while(true) {

            // try to take the semaphore

            String lockKey = keyPath + "/.lock";

            String lockKeyValue;

 

            GetValue lockKeyCOntent= consulClient.getKVValue(lockKey).getValue();

 

            if (lockKeyContent != null) {

                // lock值转换

                lockKeyValue = lockKeyContent.getValue();

                BASE64Decoder decoder = new BASE64Decoder();

                byte[] v = decoder.decodeBuffer(lockKeyValue);

                String lockKeyValueDecode = new String(v);

                logger.debug("lockKey=" + lockKey + ", lockKeyValueDecode=" + lockKeyValueDecode);

 

                Gson gson = new Gson();

                ContenderValue cOntenderValue= gson.fromJson(lockKeyValueDecode, ContenderValue.class);

                // 当前信号量已满

                if(contenderValue.getLimit() == contenderValue.getHolders().size()) {

                    logger.debug("Semaphore limited " + contenderValue.getLimit() + ", waiting...");

                    if(block) {

                        // 如果是阻塞模式,再尝试

                        try {

                            Thread.sleep(100L);

                        } catch (InterruptedException e) {

                        }

                        continue;

                    }

                    // 非阻塞模式,直接返回没有获取到信号量

                    return false;

                }

                // 信号量增加

                contenderValue.getHolders().add(sessionId);

                putParams = new PutParams();

                putParams.setCas(lockKeyContent.getModifyIndex());

                boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

                if(c) {

                    acquired = true;

                    return true;

                }

                else

                    continue;

            } else {

                // 当前信号量还没有,所以创建一个,并马上抢占一个资源

                ContenderValue cOntenderValue= new ContenderValue();

                contenderValue.setLimit(limit);

                contenderValue.getHolders().add(sessionId);

 

                putParams = new PutParams();

                putParams.setCas(0L);

                boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

                if (c) {

                    acquired = true;

                    return true;

                }

                continue;

            }

        }

    }

 

    /**

     * 创建sessionId

     * @param sessionName

     * @return

     */

    public String createSessionId(String sessionName) {

        NewSession newSession = new NewSession();

        newSession.setName(sessionName);

        return consulClient.sessionCreate(newSession, null).getValue();

    }

 

    /**

     * 释放session、并从lock中移除当前的sessionId

     * @throws IOException

     */

    public void release() throws IOException {

        if(this.acquired) {

            // remove session from lock

            while(true) {

                String cOntenderKey= keyPath + "/" + sessionId;

                String lockKey = keyPath + "/.lock";

                String lockKeyValue;

 

                GetValue lockKeyCOntent= consulClient.getKVValue(lockKey).getValue();

                if (lockKeyContent != null) {

                    // lock值转换

                    lockKeyValue = lockKeyContent.getValue();

                    BASE64Decoder decoder = new BASE64Decoder();

                    byte[] v = decoder.decodeBuffer(lockKeyValue);

                    String lockKeyValueDecode = new String(v);

                    Gson gson = new Gson();

                    ContenderValue cOntenderValue= gson.fromJson(lockKeyValueDecode, ContenderValue.class);

                    contenderValue.getHolders().remove(sessionId);

                    PutParams putParams = new PutParams();

                    putParams.setCas(lockKeyContent.getModifyIndex());

                    consulClient.deleteKVValue(contenderKey);

                    boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

                    if(c) {

                        break;

                    }

                }

            }

            // remove session key

 

        }

        this.acquired = false;

        clearSession();

    }

 

    public void clearSession() {

        if(sessionId != null) {

            consulClient.sessionDestroy(sessionId, null);

            sessiOnId= null;

        }

    }

 

    class ContenderValue implements Serializable {

 

        private Integer limit;

        private List holders = new ArrayList<>();

 

        public Integer getLimit() {

            return limit;

        }

 

        public void setLimit(Integer limit) {

            this.limit = limit;

        }

 

        public List getHolders() {

            return holders;

        }

 

        public void setHolders(List holders) {

            this.holders = holders;

        }

 

        @Override

        public String toString() {

            return new Gson().toJson(this);

        }

 

    }

}

单元测试

下面单元测试的逻辑:通过线程的方式来模拟不同的分布式服务来获取信号量执行业务逻辑。由于信号量与简单的分布式互斥锁有所不同,它不是只限定一个线程可以操作,而是可以控制多个线程的并发,所以通过下面的单元测试,我们设置信号量为3,然后同时启动15个线程来竞争的情况,来观察分布式信号量实现的结果如何。

 

public class TestLock {

 

    private Logger logger = Logger.getLogger(getClass());

 

    @Test

    public void testSemaphore() throws Exception {

        new Thread(new SemaphoreRunner(1)).start();

        new Thread(new SemaphoreRunner(2)).start();

        new Thread(new SemaphoreRunner(3)).start();

        new Thread(new SemaphoreRunner(4)).start();

        new Thread(new SemaphoreRunner(5)).start();

        new Thread(new SemaphoreRunner(6)).start();

        new Thread(new SemaphoreRunner(7)).start();

        new Thread(new SemaphoreRunner(8)).start();

        new Thread(new SemaphoreRunner(9)).start();

        new Thread(new SemaphoreRunner(10)).start();

        Thread.sleep(1000000L);

    } 

}

  

public class SemaphoreRunner implements Runnable {

 

    private Logger logger = Logger.getLogger(getClass()); 

    private int flag;

 

    public SemaphoreRunner(int flag) {

        this.flag = flag;

    }

 

    @Override

    public void run() {

        Semaphore semaphore = new Semaphore(new ConsulClient(), 3, "mg-init");

        try {

            if (semaphore.acquired(true)) {

                // 获取到信号量,执行业务逻辑

                logger.info("Thread " + flag + " start!");

                Thread.sleep(new Random().nextInt(10000));

                logger.info("Thread " + flag + " end!");

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                // 信号量释放、Session锁释放、Session删除

                semaphore.release();

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

 执行结果:

INFO  [Thread-6] SemaphoreRunner - Thread 7 start!

 INFO  [Thread-2] SemaphoreRunner - Thread 3 start!

 INFO  [Thread-7] SemaphoreRunner - Thread 8 start!

 INFO  [Thread-2] SemaphoreRunner - Thread 3 end!

 INFO  [Thread-5] SemaphoreRunner - Thread 6 start!

 INFO  [Thread-6] SemaphoreRunner - Thread 7 end!

 INFO  [Thread-9] SemaphoreRunner - Thread 10 start!

 INFO  [Thread-5] SemaphoreRunner - Thread 6 end!

 INFO  [Thread-1] SemaphoreRunner - Thread 2 start!

 INFO  [Thread-7] SemaphoreRunner - Thread 8 end!

 INFO  [Thread-10] SemaphoreRunner - Thread 11 start!

 INFO  [Thread-10] SemaphoreRunner - Thread 11 end!

 INFO  [Thread-12] SemaphoreRunner - Thread 13 start!

 INFO  [Thread-1] SemaphoreRunner - Thread 2 end!

 INFO  [Thread-3] SemaphoreRunner - Thread 4 start!

 INFO  [Thread-9] SemaphoreRunner - Thread 10 end!

 INFO  [Thread-0] SemaphoreRunner - Thread 1 start!

 INFO  [Thread-3] SemaphoreRunner - Thread 4 end!

 INFO  [Thread-14] SemaphoreRunner - Thread 15 start!

 INFO  [Thread-12] SemaphoreRunner - Thread 13 end!

 INFO  [Thread-0] SemaphoreRunner - Thread 1 end!

 INFO  [Thread-13] SemaphoreRunner - Thread 14 start!

 INFO  [Thread-11] SemaphoreRunner - Thread 12 start!

 INFO  [Thread-13] SemaphoreRunner - Thread 14 end!

 INFO  [Thread-4] SemaphoreRunner - Thread 5 start!

 INFO  [Thread-4] SemaphoreRunner - Thread 5 end!

 INFO  [Thread-8] SemaphoreRunner - Thread 9 start!

 INFO  [Thread-11] SemaphoreRunner - Thread 12 end!

 INFO  [Thread-14] SemaphoreRunner - Thread 15 end!

 INFO  [Thread-8] SemaphoreRunner - Thread 9 end!

从测试结果,我们可以发现当信号量持有者数量达到信号量上限3的时候,其他竞争者就开始进行等待了,只有当某个持有者释放信号量之后,才会有新的线程变成持有者,从而开始执行自己的业务逻辑。所以,分布式信号量可以帮助我们有效的控制同时操作某个共享资源的并发数。

优化建议与参考文档

同前文一样,这里只是做了简单的实现。线上应用还必须加入TTL的session清理以及对.lock资源中的无效holder进行清理的机制。

 

参考文档:

https://www.consul.io/docs/guides/semaphore.html

转自:http://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247483857&idx=1&sn=495c0faad9bc237132aca49e722022ec&chksm=9bd0ac49aca7255fec67f9364fab63638b30e7a69fc0771f5977a6cc9a38856879b64832bc67&scene=21#wechat_redirect

 

推荐阅读
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • 服务器部署中的安全策略实践与优化
    服务器部署中的安全策略实践与优化 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 在Kohana 3框架中,实现最优的即时消息显示方法是许多开发者关注的问题。本文将探讨如何高效、优雅地展示flash消息,包括最佳实践和技术细节,以提升用户体验和代码可维护性。 ... [详细]
  • HBase Java API 进阶:过滤器详解与应用实例
    本文详细探讨了HBase 1.2.6版本中Java API的高级应用,重点介绍了过滤器的使用方法和实际案例。首先,文章对几种常见的HBase过滤器进行了概述,包括列前缀过滤器(ColumnPrefixFilter)和时间戳过滤器(TimestampsFilter)。此外,还详细讲解了分页过滤器(PageFilter)的实现原理及其在大数据查询中的应用场景。通过具体的代码示例,读者可以更好地理解和掌握这些过滤器的使用技巧,从而提高数据处理的效率和灵活性。 ... [详细]
  • 用阿里云的免费 SSL 证书让网站从 HTTP 换成 HTTPS
    HTTP协议是不加密传输数据的,也就是用户跟你的网站之间传递数据有可能在途中被截获,破解传递的真实内容,所以使用不加密的HTTP的网站是不 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • 本文详细介绍了 InfluxDB、collectd 和 Grafana 的安装与配置流程。首先,按照启动顺序依次安装并配置 InfluxDB、collectd 和 Grafana。InfluxDB 作为时序数据库,用于存储时间序列数据;collectd 负责数据的采集与传输;Grafana 则用于数据的可视化展示。文中提供了 collectd 的官方文档链接,便于用户参考和进一步了解其配置选项。通过本指南,读者可以轻松搭建一个高效的数据监控系统。 ... [详细]
  • 解决针织难题:R语言编程技巧与常见错误分析 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 在前后端分离的架构中,使用Shiro框架进行权限管理时,遇到了Session存储的问题。具体表现为在尝试通过Session保存某个键值时,总是无法成功获取该键值。经过调试发现,Shiro框架在处理请求时会对Spring MVC的Request对象进行封装,导致Session操作出现异常。为了解决这一问题,可以通过自定义Session管理器或调整Shiro的配置,确保Session数据能够正确存储和读取。此外,还可以考虑使用分布式Session存储方案,如Redis,以提高系统的可扩展性和可靠性。 ... [详细]
author-avatar
手机用户2602940113
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有