这是JWT认证条件下的getCurrentLoginUser代码实现,请分析性能:
在生产环境中,currentLoginUser永远为null,if不执行。
执行else内的解析JWT的代码,解析userId,再查询用户。
很明显,一次请求内,当getCurrentLoginUser被多次调用时,会重复解析JWT,就会产生性能问题。
解决分析
解决重复解析JWT的唯一思路就是缓存,第一次解析完userId并查询出user后将这个user对象缓存。
因为并发请求时,每个请求分配一个线程管理Socket。
所以当前待解决的问题就变成了:如何设计一种缓存,使之各线程不影响,线程安全。
简单的设计如上,一个Map,Thread作为key,用户缓存作为value。
ThreadLocal
ThreadLocal是啥?没听说过是不是?其实这是Java里最基础的东西。我的Java到底学了个啥呀?
日常吐槽,我发现其他学校竟然讲spring-boot、spring-cloud、ConcurrentHashMap源码,人家上完专业课直接精通kafka。
一般人知道HashMap的阀值为什么是8吗?
对不起,这些人家老师都讲过。而《河北工业大学》的“大博士”,你讲个检查异常都讲错了,被学生指出之后还不改。你去学学Java再来讲课好吗?
最后的结果就是,我们辛辛苦苦准备了好几个月的东西,人家课上就精通完了。怪不得干不过人家,我们引以为傲的spring-cloud、RPC原来都属于课上的基础知识。
继续。
我们想用一个类似Map<Thread, User>这样的数据结构来设计缓存,其实JDK中早就为我们封装好了,即ThreadLocal<T>。
栗子
大家来看下面的示例代码:
运行结果如下:
main线程创建ThreadLocal对象,thread1、thread2操作的是同一个对象local,thread1、thread2分别set数据,两线程再次从local中获取数据的时候,能够保证两者数据不冲突。
底层原理
ThreadLocal中的set方法实现如下:
获取当前线程,同时通过线程对象获取ThreadLocalMap。
set内调用了getMap方法,看看getMap的内部实现:
返回线程对象内的threadLocals属性。
Thread类中的成员属性threadLocals,默认为null。
接着看:获取到了Map之后,用当前的ThreadLocal对象作为key存储value进Map。
这样设计十分地精妙,每一个线程都有独立的Map存储,肯定能做到数据隔离且安全。且可方便地创建多个安全的ThreadLocal进行存储。
改写
鉴于ThreadLocal的特性,我们可以设计一个SecurityContext以封装ThreadLocal<User>。
写一个拦截器,思路如下:
在pre里解析JWT,并存到SecurityContext里。
在post里clear,防止线程池线程复用导致数据错误。
然后原来的getCurrentLoginUser方法直接从SecurityContext中get即可。
熟悉吗?
看到SecurityContext这个名称是不是很熟悉?
从spring-security中获取用户信息的方法如下,它怎么实现的呢?
点开spring-security源码,有三种策略:GlobalSecurityContextHolderStrategy(即全局线程共享策略)、ThreadLocalSecurityContextHolderStrategy(本地策略)、InheritableThreadLocalSecurityContextHolderStrategy(可继承的本地策略)。
点开源码后发现,其实spring-security就是这么简单,内部也是用ThreadLocal实现的,只是此处存储的信息比较多,使用ThreadLocal<SecurityContext>。