本文概览:介绍通过redis实现分布式锁。
分布锁还有一个作用就是实现频控,假设超时是2s,如果不释放锁就相当于2s只能操作一次。
1 介绍
1.1 应用场景
用户在进行购买操作时,在某一个时刻,只容许用户进行一次购买,此时可以按照用户维度添加一个分布式锁来实现。
1.2 相关的redis命令
1、setnx
setnx的命令实现了“将 key 的值设为 value ,当且仅当 key 不存在”功能,所以可以通过setnx来实现锁
命令格式为:
| 
					 1  | 
						SETNX key value  | 
					
举例:
| 
					 1 2 3 4 5 6 7 8  | 
						127.0.0.1:6379> exists lockkey     #检查是否存在key (integer) 0 127.0.0.1:6379> setnx lockeky 111    #为key进行赋值 (integer) 1 127.0.0.1:6379> setnx lockeky 1112  #再次为key赋值失败 (integer) 0 127.0.0.1:6379> get lockeky      #获取key的值 "111"  | 
					
2、del
可以用来实现释放锁
del命令格式为
| 
					 1  | 
						DEL key [key ...]  | 
					
3、exipire
可以用来实现对一个锁进行设置到期时间。
命令格式如下:
| 
					 1  | 
						EXPIRE key seconds  | 
					
4. cad
CAD(Compare And Delete),查看目标key的value是否等于指定的value值,如果相等,删除该key;不相等则不删除。
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  | 
						    // 创建jedis     private Jedis jedis = new Jedis("127.0.0.1", 6379);     /**      * 获取锁      * @param lockKey      * @param  expires      * @return      */     public boolean getLock(String lockKey,int expires) {         try {             Long result = jedis.setnx(lockKey, "1");             if (result == 1) {                 // 设置过期时间                 jedis.expire(key, expires);                 return true;             } else {                 return false;             }         }catch (Exception e){             LOGGER.error("get lock error,key={}",lockKey,e);             return false;         }     }     /**      * 通过重试和获取锁      * @param lockKey      * @param expires 单位秒      * @return      */     @Retryable(maxAttempts = 3,             backoff = @Backoff(delay = 500), // 时间单位:毫秒             value = {RuntimeException.class})     public boolean getLockWithRetry(String lockKey,int expires) {             boolean isGetLock = getLock(lockKey, expires);             if (isGetLock) {                 LOGGER.debug("getLockWithRetry success expires={},lockKey={}", expires, lockKey);                 return true;             } else {                 // 需要重试。这个考虑线上错误会很多暂时不打印log                 LOGGER.debug("get lock retry error,locKey:{}", lockKey);                 throw new RuntimeException("get lock with retry exception");             }     }     /**      * 释放锁。如果释放抛异常了,则此时就等待锁到期之后,自动释放      * @param key      * @return      */     public boolean releaseLock(String key) {         try {             Long result = jedis.del(key);             if (result == 1) {                 return true;             } else {                 return false;             }         }catch (Exception e){             LOGGER.error("release lock error,key={}",key,e);             return false;         }     }  | 
					
1、保证超时和上锁在一个事务。超时为了防止上锁的线程在释放锁时突然中断,导致释放锁失败,加入一个key的过期时间。
在getLock的逻辑中,对于如下两个操作:
| 
					 1 2 3 4 5 6  | 
						    Long result = jedis.setnx(lockKey, "1");     if (result == 1) {         // 设置过期时间         jedis.expire(key, expires);          return true;      }  | 
					
可以通过如下操作来实现(从 Redis 2.6.12 版本开始)。参考 http://doc.redisfans.com/string/set.html
| 
					 1  | 
						jedis.set(key, value, nxxx, expx, time);  | 
					
此时修改为
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | 
						    /**      * 获取锁      * @param lockKey      * @param  expires      * @return      */     public boolean getLock(String lockKey,int expires) {         try {             String ret =jedis.set(lockKey, "1", "NX", "EX", expires);             LOGGER.debug("get lock lockKey={},ret={}", lockKey, ret);             if (StringUtils.equalsIgnoreCase(ret, "OK")) {                 return true;             } else {                 return false;             }         }catch (Exception e){             LOGGER.error("get lock error,key={}",lockKey,e);             return false;         }     }  | 
					
2、如何避免释放别人的锁
key可以通过uuid来生成,在释放锁的时候可以通过如下代码
| 
					 1 2 3 4 5  | 
						if (jedis.get(key).equals(uuid)) {     jedis.del(key);     return true; } return false;  | 
					
也可以通过cad来实现。CAD(Compare And Delete),查看目标key的value是否等于指定的value值,如果相等,删除该key;不相等则不删除。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  | 
						    /**      * 释放锁。如果释放抛异常了,则此时就等待锁到期之后,自动释放      * @param key      * @return      */     public boolean releaseLock(String key) {         try {             Long result = jedis.cad(key);             if (result == 1) {                 return true;             } else {                 return false;             }         }catch (Exception e){             LOGGER.error("release lock error,key={}",key,e);             return false;         }     }  | 
					
3 应用
释放锁的场景有两种:finnally中释放(成功或者异常都释放) 和 只有执行成功之后释放。对于只有成功之后释放锁的场景,如果失败了,只能等到锁过期自动释放了。
以在finnaly中释放为例:
在砸金蛋活动接口中用到了分布式锁,因为每次砸蛋都需要更新用户砸蛋次数,为了避免用户并发砸蛋,需要使用分布式锁。如下
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						       try {             if (redisLockService.getLock(lockKey,60)) {                 砸蛋更新用户信息的逻辑             } else {                 throw new RuntimeException("稍后重试");             }         } catch (Exception e) {             ......         } finally {             redisLockService.releaseLock(lockKey);         }  | 
					
4 问题
A:如果加锁超时,返回失败,进行删除自己锁时,如何保证删除自己加的锁。
Q: 设置value为UUID,然后删除之前,判断下value的值是否为UUID。 可以使用Resis的cad命令代替del命令
(全文完)






