目前开发过程当中,按照公司规范,须要依赖框架中的缓存组件。不得不说,作组件的大牛对CRUD操做的封装,链接池、缓存路由、缓存安全性的管控都处理的无可挑剔。可是有一个小问题,该组件没有对分布式锁作实现,那就要想办法依靠缓存组件本身去实现一个分布式锁了。redis
什么,为啥要本身实现?有现成的开源组件直接拿过来用不就好了,好比Spring-Integration-Redis提供RedisLockRegistry,Redisson,不比本身去实现快的多。那我得声明一下,本人也不喜欢重复造轮子。具体缘由呢,首先是项目中的缓存组件是不能替换的,链接池还可能没有办法复用,其次就是若是对开源组件实现原理不熟悉,那么出了问题,维护起来又须要更多成本。spring
先说一下当前须要分布式锁的两个场景,一个是微信端access_token刷新(分布式锁能够保证access_token只刷新一次,刷新完成以后放入缓存,其余请求直接从缓存读取);一个是分布式部署的定时任务(分布式锁能够保证同一时刻只有一个节点的定时任务执行)。sql
在单机部署的状况下,要想保证特定业务在顺序执行,经过JDK提供的synchronized关键字、Semaphore、ReentrantLock,或者咱们也能够基于AQS定制化锁。单机部署的状况下,锁是在多线程之间共享的,可是分布式部署的状况下,锁是多进程之间共享的。那么分布式锁要保证锁资源的惟一性,能够在多进程之间共享。数据库
RedisLockRegistry是spring-integration-redis中提供redis分布式锁实现类。主要是经过redis锁+本地锁双重锁的方式实现的一个比较好的锁。小程序
OBTAIN_LOCK_SCRIPT是一个上锁的lua脚本。KEYS[1]表明当前锁的key值,ARGV[1]表明当前的客户端标识,ARGV[2]表明过时时间。微信小程序
基本逻辑是:根据KEYS[1]从redis中拿到对应的客户端标识,如已存在的客户端标识和ARGV[1]相等,那么重置过时时间为ARGV[2];若是值不存在,设置KEYS[1]对应的值为ARGV[1],而且过时时间是ARGV[2]。api
获取锁的过程也很简单,首先经过本地锁(localLock,对应的是ReentrantLock实例)获取锁,而后经过RedisTemplate执行OBTAIN_LOCK_SCRIPT脚本获取redis锁。缓存
为何要使用本地锁呢,首先是为了锁的可重入,其次是减轻redis服务压力。安全
释放锁的过程也比较简单,第一步经过本地锁判断当前线程是否持有锁,第二步经过本地锁判断当前线程持有锁的计数。微信
若是当前线程持有锁的计数 > 1,说明本地锁被当前线程屡次获取,这时只释放本地锁(释放以后当前线程持有锁的计数-1)。
若是当前线程持有锁的计数 = 1,释放本地锁和redis锁。
RedisLockRegistry使用如上所示。
首先定义RedisLockRegistry对应的Bean,须要依赖redis的ConnectionFactory。
而后在服务层中注入RedisLockRegistry实例。
经过lock方法和unlock方法将业务逻辑包起来,须要注意的是unlock方法要写在finally代码块中。
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。
充分的利用了Redis键值数据库提供的一系列优点,基于Java实用工具包中经常使用接口,为使用者提供了一系列具备分布式特性的经常使用工具类。
使得本来做为协调单机多线程并发程序的工具包得到了协调分布式多机多线程并发系统的能力,大大下降了设计和研发大规模分布式系统的难度。
同时结合各富特点的分布式服务,更进一步简化了分布式环境中程序相互之间的协做。
首先感觉一下经过Redisson Api使用redis分布式锁。
定义RedissonBuilder,经过redis集群地址构建RedissonClient。
定义RedissonClient类型的Bean。
业务代码里,经过RedissonClient获取分布式锁。
因为对Redisson分布式锁实现原理了解的也不是很透彻,这里推荐一篇文章:Redisson 分布式锁实现分析。
本地锁(ReentrantLock)+ redis锁
每个lock key对应惟一的一个本地锁
分布式环境下,每个线程对应一个惟一标识
经过JDK ConcurrentTaskScheduler完成定时任务执行,ScheduledFuture完成定时任务销毁。其中taskId对应线程标识。
经过RedisLock注解实例lockInfo获取到锁key值、锁过时时间信息。
定义测试类,测试方法注上@RedisLock注解,制定锁的key值为 "redis-lock-test",测试方法内随机休眠。
开启20个线程,同时调用测试方法。
多线程redis分布式锁测试结果以下。
定义可重入测试类,方法内获取当前代理对象,递归调用测试方法。
测试方法中,调用可重入测试类注有@RedisLock的测试方法。
分布式锁可重入测试结果以下。
refreshAccessToken方法上标注@RedisLock注解,代表此方法在分布式环境下会串行执行。
首先从缓存里获取access_token。
若是缓存里的access_token为空或者和失效的access_token相等,经过TokenAPI生成新的access_token并放入缓存。
若是缓存里的access_token不为空而且和失效的access_token不相等,直接返回缓存里的access_token。
若是缓存中的access_token为空,直接刷新access_token并放入缓存。
若是缓存中的access_token不为空且和失效的access_token相等则刷新access_token并放入缓存,不然直接返回缓存中的access_token。
在分布式环境下,涉及线程间并发问题和进程间并发问题都是能够经过分布式锁解决的。若是是单节点线程之间共享资源的并发问题能够经过JDK提供的线程锁来解决,若是是多节点多线程之间共享资源的并发问题就须要借助分布式锁。好比最多见的秒杀、抢红包,后台服务中涉及到库存扣减、金额扣减、以及其余高并发串行化场景的操做均可用分布式锁来解决问题。本文讲述的例子主要是应用在微信公众号和微信小程序access_token刷新、微信分享jsapi_ticket刷新,分布式锁能够保证access_token和jsapi_ticket在高并发下只有一个线程去执行刷新动做,避免屡次刷新后access_token或者jsapi_ticket失效的问题。