使用场景:
在tomcat集群环境下,有任务调度,也就是定时任务
同一时间多个tomcat执行同一个任务,如定时关单.
如果有大量订单需要关闭,则每个tomcat都会执行相同数量的关单sql,
这样是非常浪费资源的,如果还有相应的记录的话 则会导致重复记录的出现.
这个时候redis分布式锁就派上用场了,通过这个锁,可以随机的让单个tomcat去执行关单操作,而其余的tomcat则不会执行关单操作.
原理:
主要是使用这几个redis命令来实现:
1.setnx : 不能设置重复key
2.getset : 获取旧的值,设置新的值
3.expire : 设置key的有效期
4.del : 删除key
首先使用setnx命令保存一个key,value
setnx(lockkey,currentTime+timeout)
1
lockkey : 就是key的名称
currentTime : 时间戳(System.currentTimeMillis())
timeout : 这个锁被动释放的时间,定义在配置文件中,方便修改
1
2
3
如果设置成功,也就是返回1,给这个key设置有效期
expire(lockkey,timeout)
1
接着执行业务,如调用关单的sql
最后删除key,也就是释放锁
del(lockkey)
1
如果设置失败,也就是返回0,代表当前有tomcat正在使用锁,还没有释放
那就获取当前锁
get(lockkey) 得到valueA
1
如果
valueA!=null && currentTime (当前时间毫秒数)>valueA
1
代表这个key已经超时了,
这个时候获取到这个锁的tomcat有权重新设置超时时间,也就是重新设置value
getset(lockkey,currentTime+timeout) 得到valueB
1
执行完后 返回valueB,如果
valueB ==null || valueA(之前get得到的值) == valueB
1
那么便是成功的获取到锁,执行获取到锁的流程
否则便结束这次定时任务
---------------------
参考资料:https://redis.io/commands/setnx
加锁是为了解决多线程的资源共享问题。Java中,单机环境的锁可以用synchronized和Lock,其他语言也都应该有自己的加锁机制。但是到了分布式环境,单机环境中的锁就没什么作用了,因为每个节点只能获取到自己机器内存中的锁,而无法获取到其他节点的锁状态。
分布式环境中,应该用专门的分布式锁来解决需要加锁的问题。分布式锁有很多实现,Redis,zookeeper都可以。这里以Redis为例,讲述一下基于Redis的分布式锁的基本原理。
用Redis来实现分布式锁的原因
不同的节点无法获取到其他节点内存中的锁,但是大家都可以获取到Redis中的资源,所以这是实现分布式锁的基础-所有节点都可以同时获取到redis的状态。
而具体的实现,则是基于两个redis的命令-SETNX和GETSET。
SETNX:SET if Not eXists,格式为SETNX key value,仅当key不存在时才会设置成功,返回1,否则返回0。这是加锁的基础,假设key名为lock.foo,只要有一个线程设置成功,那其他线程都无法再设置。
GETSET:GETSET key value,返回旧值,并将新的值设置进去。这个的作用后面会讲到。
锁实现以及超时设计
加锁方式很简单,在线程中对redis发送一个命令:
SETNX lock.foo <current Unix time + lock timeout + 1>
线程A调用setnx命令,设置key为lock.foo(所有线程要用同样的key,否则就不是一个锁了),值为current Unix time + lock timeout + 1,即当前时间加上加锁时长,最终的值也就是过期时间。如果A对锁的持有结束,则可自行调用del lock.foo来释放锁。
A持有锁的过程中线程B在调用命令SETNX lock.foo,会得到返回值0,这说明这个锁已经被其他线程获取,这时B应该去获取lock.foo的值,看看是否小于当前时间,如果大于则锁未过期,B需要继续循环等待检查或者做其他操作;如果小于则锁已过期,B可以用del lock.foo方法去删除锁,然后在SETNX lock.foo 来获取锁。
这样就完成了分布式锁的最基本的模型,并且避免了因A线程挂掉无法释放锁而导致的死锁问题。
存在的问题
上一节的实现看上去大致还是那么回事,成功的加上锁了,还引入了超时机制。不过,GETSET还没用呢,这肯定还没完呢。请看以下场景:
A获取到了锁,但是挂掉了;
B和C都检测到A的锁超时;
B发出del lock.foo指令,删除A的锁,再setnx,获取到了锁;
C也发出del lock.foo指令,此时删除的是B的锁,然后再setnx,获取到了锁。
这个时候你会发现,B和C同时获取到了锁。这问题就大了去了。
为了解决这个问题,GETSET就起到他自己的作用了。下面用修正后的方法来重新描述一下上面的场景:
A获取到了锁,但是挂掉了;
B和C都检测到A的锁超时;
此时B不会执行del操作,而是执行:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
这个命令会给lock.foo设置新值,然后获取到老的value。这个时候B会对老的value进行检测,如果value大于当前时间,则说明这个锁已经被其他线程再次获取了,那B就会继续
等待,而不是获取锁。如果value小于当前时间,那B就可以获取到锁。
假设C获取到锁,然后B又再次调用GETSET方法,那也不会对C持有锁造成影响,不过确实会将超时时间延长一些。但是出现这个情况肯定是B和C都在之前检测到了锁超时,说明这两个线程对锁的访问肯定较为接近,所以这里如果要求不是太严格也可以忽略。
* 在集群等多服务器中经常使用到同步处理一下业务,这是普通的事务是满足不了业务需求,需要分布式锁
*
* 分布式锁的常用3种实现:
* 0.数据库乐观锁实现
* 1.Redis实现
--- 使用redis的setnx()、get()、getset()方法,用于分布式锁,解决死锁问题
* 2、zookeeper实现
Zookeeper实现
* 参考:http://surlymo.iteye.com/blog/2082684
* http://www.jb51.net/article/103617.htm
* http://www.hollischuang.com/archives/1716?utm_source=tuicool&utm_medium=referral
1、实现原理:
基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下(该图来自于IBM网站)。大致思想即为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
2、优点
锁安全性高,zk可持久化
3、缺点
性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。
4、实现
可以直接采用zookeeper第三方库curator即可方便地实现分布式锁
Redis实现分布式锁的原理:
* 1.通过setnx(lock_timeout)实现,如果设置了锁返回1, 已经有值没有设置成功返回0
* 2.死锁问题:通过实践来判断是否过期,如果已经过期,获取到过期时间get(lockKey),然后getset(lock_timeout)判断是否和get相同,
* 相同则证明已经加锁成功,因为可能导致多线程同时执行getset(lock_timeout)方法,这可能导致多线程都只需getset后,对于判断加锁成功的线程,
* 再加expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS)过期时间,防止多个线程同时叠加时间,导致锁时效时间翻倍
* 3.针对集群服务器时间不一致问题,可以调用redis的
time
()获取当前时间
2.Redis分分布式锁的代码实现
1.定义锁接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package com.jay.service.redis; /** * Redis分布式锁接口 * Created by hetiewei on 2017/4/7. */ public interface RedisDistributionLock { /** * 加锁成功,返回加锁时间 * @param lockKey * @param threadName * @return */ public long lock(String lockKey, String threadName); /** * 解锁, 需要更新加锁时间,判断是否有权限 * @param lockKey * @param lockValue * @param threadName */ public void unlock(String lockKey, long lockValue, String threadName); /** * 多服务器集群,使用下面的方法,代替System.currentTimeMillis(),获取redis时间,避免多服务的时间不一致问题!!! * @return */ public long currtTimeForRedis(); } |
2.定义锁实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | package com.jay.service.redis.impl; import com.jay.service.redis.RedisDistributionLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import java.util.concurrent.TimeUnit; /** * Created by hetiewei on 2017/4/7. */ public class RedisLockImpl implements RedisDistributionLock { //加锁超时时间,单位毫秒, 即:加锁时间内执行完操作,如果未完成会有并发现象 private static final long LOCK_TIMEOUT = 5 * 1000 ; private static final Logger LOG = LoggerFactory.getLogger(RedisLockImpl. class ); private StringRedisTemplate redisTemplate; public RedisLockImpl(StringRedisTemplate redisTemplate) { this .redisTemplate = redisTemplate; } /** * 加锁 * 取到锁加锁,取不到锁一直等待知道获得锁 * @param lockKey * @param threadName * @return */ @Override public synchronized long lock(String lockKey, String threadName) { LOG.info(threadName+ "开始执行加锁" ); while ( true ){ //循环获取锁 //锁时间 Long lock_timeout = currtTimeForRedis()+ LOCK_TIMEOUT + 1 ; if (redisTemplate.execute( new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { //定义序列化方式 RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); byte [] value = serializer.serialize(lock_timeout.toString()); boolean flag = redisConnection.setNX(lockKey.getBytes(), value); return flag; } })){ //如果加锁成功 LOG.info(threadName + "加锁成功 ++++ 111111" ); //设置超时时间,释放内存 redisTemplate.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS); return lock_timeout; } else { //获取redis里面的时间 String result = redisTemplate.opsForValue().get(lockKey); Long currt_lock_timeout_str = result== null ? null :Long.parseLong(result); //锁已经失效 if (currt_lock_timeout_str != null && currt_lock_timeout_str < System.currentTimeMillis()){ //判断是否为空,不为空时,说明已经失效,如果被其他线程设置了值,则第二个条件判断无法执行 //获取上一个锁到期时间,并设置现在的锁到期时间 Long old_lock_timeout_Str = Long.valueOf(redisTemplate.opsForValue().getAndSet(lockKey, lock_timeout.toString())); if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)){ //多线程运行时,多个线程签好都到了这里,但只有一个线程的设置值和当前值相同,它才有权利获取锁 LOG.info(threadName + "加锁成功 ++++ 22222" ); //设置超时间,释放内存 redisTemplate.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS); //返回加锁时间 return lock_timeout; } } } try { LOG.info(threadName + "等待加锁, 睡眠100毫秒" ); // TimeUnit.MILLISECONDS.sleep(100); TimeUnit.MILLISECONDS.sleep( 200 ); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 解锁 * @param lockKey * @param lockValue * @param threadName */ @Override public synchronized void unlock(String lockKey, long lockValue, String threadName) { LOG.info(threadName + "执行解锁==========" ); //正常直接删除 如果异常关闭判断加锁会判断过期时间 //获取redis中设置的时间 String result = redisTemplate.opsForValue().get(lockKey); Long currt_lock_timeout_str = result == null ? null :Long.valueOf(result); //如果是加锁者,则删除锁, 如果不是,则等待自动过期,重新竞争加锁 if (currt_lock_timeout_str != null && currt_lock_timeout_str == lockValue){ redisTemplate.delete(lockKey); LOG.info(threadName + "解锁成功------------------" ); } } /** * 多服务器集群,使用下面的方法,代替System.currentTimeMillis(),获取redis时间,避免多服务的时间不一致问题!!! * @return */ @Override public long currtTimeForRedis(){ return redisTemplate.execute( new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.time(); } }); } } |
3.分布式锁验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | @RestController @RequestMapping ( "/distribution/redis" ) public class RedisLockController { private static final String LOCK_NO = "redis_distribution_lock_no_" ; private static int i = 0 ; private ExecutorService service; @Autowired private StringRedisTemplate redisTemplate; /** * 模拟1000个线程同时执行业务,修改资源 * * 使用线程池定义了20个线程 * */ @GetMapping ( "lock1" ) public void testRedisDistributionLock1(){ service = Executors.newFixedThreadPool( 20 ); for ( int i= 0 ;i< 1000 ;i++){ service.execute( new Runnable() { @Override public void run() { task(Thread.currentThread().getName()); } }); } } @GetMapping ( "/{key}" ) public String getValue( @PathVariable ( "key" ) String key){ Serializable result = redisTemplate.opsForValue().get(key); return result.toString(); } private void task(String name) { // System.out.println(name + "任务执行中"+(i++)); //创建一个redis分布式锁 RedisLockImpl redisLock = new RedisLockImpl(redisTemplate); //加锁时间 Long lockTime; if ((lockTime = redisLock.lock((LOCK_NO+ 1 )+ "" , name))!= null ){ //开始执行任务 System.out.println(name + "任务执行中" +(i++)); //任务执行完毕 关闭锁 redisLock.unlock((LOCK_NO+ 1 )+ "" , lockTime, name); } } } |