本文概览:介绍通过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命令
(全文完)