前言
redis大部分公司都在使用,再面试redis的时候一定也会被问到的一个问题就是分布式锁。或者你的领导让你写一个分布式锁,那这篇文章肯定可以帮到你
提示:以下是本篇文章正文内容,下面案例可供参考
一、分布式锁是什么?
在多线程同是访问同一个资源的时候,由于线程的访问顺序不能确定,会造成资源同时被多个线程修改的时候数据不一致的情况,锁就是为了避免这种情况的。只有获取到锁的线程,才能访问资源。所以同一时刻只有一个线程可以访问资源。
在分布式环境中一个服务会有多个实例(多个java虚拟机),java自带的锁synchronized只能锁住一个虚拟机,在单体应用的系统中是没有问题的,在分布式系统中要用分布式锁才可以。
上述场景中请求量大的时候就非常容易出现库存为负的场景,就是因为多个线程去减库存数据不一致造成的。
如果我们能够控制请求每次只能执行一个,这一个执行完之后才能执行下一个。这样就可以避免出现多个线程抢占同一个资源的情况了。这就是分布式锁的作用。
二、redis实现分布式锁的原理
一个锁应该具备的几个特性
互斥性
如果不能互斥的话就锁不住资源了,这个是锁最基本的要求
防止死锁
在使用锁的过程中一个线程拿到锁之后,一直都释放不了,其他的线程获取不到锁,程序执行一直阻塞,这个在设计上应该避免出现。
性能好
本身加锁的行为就会降低程序的QPS,所以在获取锁和释放锁的程序最好耗时少。
基于以上几点我们用redis来设计一个分布式锁:基本的思路就是我们每次执行代码之前先在redis里面做一个标记,执行完之后把这个标记清除,如果redis里面有这个标记的话说明有线程在执行,就不能执行(获取不到锁)。没有标记的话就可以执行(可以拿到锁)。
1.每次请求接口之前先拿着自己专有的key(可以的请求的参数+当前时间一定要保证唯一,每次的请求key都是不一样的)去redis查询是否存在这个key
2.如果存在说明有线程正在执行,如果不存在就可以继续执行。
需要注意的点
redis虽然时单线程的,java是多线程的,用我们的java去操作redis的时候一定要注意获取锁和释放锁是一个原子操作,要保证原子操作需要借助域lua脚本。
三、写一个基于注解分布式锁
1.获取锁和释放锁的脚本
获取锁脚本:
local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
local expireTime=ARGV[2]
local nowTime=ARGV[3]
if redis.call('get',requestKey)
then
return 0
end
local lockedHash = redis.call('hkeys',lockedKeys)
for i=1, #lockedHash do
if string.find(requestKey,lockedHash[i]) or string.find(lockedHash[i],requestKey)
then
local lockTime = redis.call('hget',lockedKeys,lockedHash[i])
if (nowTime-lockTime) >= expireTime * 1000
then
redis.call('hdel',lockedKeys,lockedHash[i])
else
return 0
end
end
end
redis.call('set',requestKey,requestValue)
redis.call('expire',requestKey,expireTime)
redis.call('hset',lockedKeys,requestKey,nowTime)
return 1
释放锁的脚本:
local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
if redis.call('get', requestKey) == requestValue
then
redis.call('hdel', lockedKeys,requestKey)
return redis.call('del',requestKey)
else
return 0
end
把脚本放在classPath的路径下面,方便程序加载里面的内容
代码编写
编写工具类 获取锁和释放锁的代码
public class RedisLockUtils {
static final Long SUCCESS = 1L;
static final String LOCKED_HASH = "cs:lockedKeyHash";
static final String GET_LOCK_LUA_RESOURCE = "/lua/getLock.lua";
static final String RELEASE_LOCK_LUA_RESOURCE = "/lua/releaseLock.lua";
static final Logger LOG = LoggerFactory.getLogger(RedisLockUtils.class);
public RedisLockUtils() {
}
public static boolean getLock(RedisTemplate redisTemplate, String lockKey, String requestValue, Integer expireTime) {
LOG.info("start run lua script,{{}} start request lock", lockKey);
lockKey = "{" + lockKey + "}";
long start = System.currentTimeMillis();
DefaultRedisScript<Long> luaScript = new DefaultRedisScript();
luaScript.setLocation(new ClassPathResource("/lua/getLock.lua"));
luaScript.setResultType(Long.class);
Object result = redisTemplate.execute(luaScript, Arrays.asList(lockKey, lockKey + "cs:lockedKeyHash"), new Object[]{requestValue, String.valueOf(expireTime), String.valueOf(System.currentTimeMillis())});
boolean getLockStatus = SUCCESS.equals(result);
LOG.info("{{}} cost time {} ms,request lock result:{}", new Object[]{lockKey, System.currentTimeMillis() - start, getLockStatus});
return getLockStatus;
}
public static boolean releaseLock(RedisTemplate redisTemplate, String lockKey, String requestValue) {
lockKey = "{" + lockKey + "}";
DefaultRedisScript<Long> luaScript = new DefaultRedisScript();
luaScript.setLocation(new ClassPathResource("/lua/releaseLock.lua"));
luaScript.setResultType(Long.class);
Object result = redisTemplate.execute(luaScript, Arrays.asList(lockKey, lockKey + "cs:lockedKeyHash"), new Object[]{requestValue});
boolean releaseLockStatus = SUCCESS.equals(result);
LOG.info("{{}}release lock result:{}", lockKey, releaseLockStatus);
return releaseLockStatus;
}
}
编写锁的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DistributeLock {
//锁的key
String key() default "";
//唯一的标志 确保当前线程持有的锁不会被其他线程释放
String hasKey() default "";
//锁的过期时间模式5s
int expire() default 5;
}
编写锁的切面
@Aspect
@Component
public class DistributeLockAspect {
private static final Logger log = LoggerFactory.getLogger(DistributeLockAspect.class);
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
StringRedisTemplate redisTemplate;
private SpelExpressionParser spelParser = new SpelExpressionParser();
private static final String PERFIX = "#";
public DistributeLockAspect() {
}
@Pointcut("@annotation(com.*.annotation.DistributeLock)")
public void distributedLockable() {
}
@Around("distributedLockable()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
DistributeLock lockable = (DistributeLock)method.getAnnotation(DistributeLock.class);
String value = "";
List paramNameList;
if (StringUtils.startsWith(lockable.key(), "#")) {
paramNameList = Arrays.asList(signature.getParameterNames());
List<Object> paramList = Arrays.asList(joinPoint.getArgs());
EvaluationContext ctx = new StandardEvaluationContext();
for(int i = 0; i < paramNameList.size(); ++i) {
ctx.setVariable((String)paramNameList.get(i), paramList.get(i));
}
value = this.spelParser.parseExpression(lockable.key()).getValue(ctx).toString();
} else {
value = lockable.key();
}
paramNameList = null;
try {
Boolean lock = RedisLockUtils.getLock(this.redisTemplate, value, lockable.hasKey(),lockable.expire() );
if (!lock) {
return null;
} else {
Object object = joinPoint.proceed();
return object;
}
} catch (Exception var10) {
this.logger.error("分布式锁异常:" + var10.getMessage());
throw var10;
}finally {
//执行完之后都要释放锁
RedisLockUtils.releaseLock(this.redisTemplate, value, lockable.hasKey());
}
}
}
使用方式
@RestController
public class TestController {
@DistributeLock(key = "lockTest",hasKey = "lockTest",expire = 4)
@RequestMapping("/test")
public String lockTest()throws Exception{
return "SUCCESS";
}
}
总结
redis的分布式锁有一个缺点就是过期时间,不适合时间比较长的任务,zk做分布式锁可以解决这个问题。
欢迎大家关注我的微信公众号,您的关注就是我不懈的动力