介绍
单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为。而在分布式架构或者集群环境下,基于本地单机的锁无法控制分布式系统中分开部署客户端的并发行为,此时分布式锁就应运而生了。
特性(本例支持特性):
-
多进程可见性(支持):多个客户端可感知
-
互斥性(支持):作为锁,需要保证任何时刻只能有一个客户端(用户)持有锁
-
可重入(不支持): 同一个客户端在获得锁后,可以再次进行加锁。重入可用hash结构实现,每次重入value+1,思路差不多
-
高可用(支持):获取锁和释放锁的效率较高,不会出现单点故障
-
自旋(支持):当客户端加锁失败时,能够提供一种机制让客户端自动重试
使用场景
-
秒杀场景,要求并发量很高,两个人人同时下单那么同一件商品只能被一个用户抢到,类似的需求都会用到分布式锁
-
定时任务(项目集群被坑过,没经验)
lua脚本
lua脚本,保证加锁和解锁的原子性
// 加锁
if redis.call('setNx',KEYS[1],ARGV[1]) then
if redis.call('get',KEYS[1])==ARGV[1] then
return redis.call('pexpire',KEYS[1],ARGV[2])
else
return 0
end
end
// 解锁
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
/**
* redis分布式锁,不支持重入和redis集群
*/
public class RedisLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockKey;
private String lockValue;
private long expireTime = 30000L; //默认锁过期时间 30s
private long outTime = 5000L; //默认获取锁超时时间 5s
private long retyTime = 500L; //重试时间0.5s,(单位毫秒)
private static final String LOCK_SUCCESS = "1";
private static final String TRY_LOCK_SCRIPT = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('pexpire',KEYS[1],ARGV[2]) else return 0 end end";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* @param lockKey key
* @param redisTemplate
*/
public RedisLock(String lockKey, StringRedisTemplate redisTemplate) {
this.lockKey = lockKey;
this.redisTemplate = redisTemplate;
}
/**
* @param lockKey key
* @param expireTime 锁过期时间
* @param redisTemplate
*/
public RedisLock(String lockKey, long expireTime, StringRedisTemplate redisTemplate) {
this(lockKey,redisTemplate);
this.expireTime = expireTime;
}
/**
*
* @param lockKey key
* @param expireTime 过期时间
* @param outTime 请求超时时间
* @param retyTime 重试时间间隔
* @param redisTemplate
*/
public RedisLock(String lockKey, long expireTime, long outTime, long retyTime, StringRedisTemplate redisTemplate) {
this(lockKey,expireTime,redisTemplate);
this.outTime = outTime;
this.retyTime = retyTime;
}
@SuppressWarnings("unchecked")
@Override
public boolean lock() {
// 加锁客户端唯一标识,UUID。
this.lockValue = IdUtil.getSnowflake(1L, 1L).nextIdStr();
Object result = redisTemplate.execute(RedisScript.of(TRY_LOCK_SCRIPT, Long.class), Collections.singletonList(lockKey), lockValue, String.valueOf(this.expireTime));
return LOCK_SUCCESS.equals(String.valueOf(result));
}
@Override
public boolean tryLock(long tryLockTime, TimeUnit timeUnit) {
// 是否死循环获取锁
boolean forever = tryLockTime < 0;
// 开始获取锁的时间
final long startTime = System.currentTimeMillis();
tryLockTime = (tryLockTime < 0) ? 0:tryLockTime;
// 统一转为毫秒,阻塞毫秒数
final Long tryTime = (timeUnit != null) ? timeUnit.toMillis(tryLockTime) : 0;
// 如果没有加锁成功,循环尝试获取锁
while (true) {
// 获取成功 退出
if (lock()) {
return Boolean.TRUE;
}
// 如果不是必须获取到锁,超过了获取锁的最长时间,退出
if (!forever && System.currentTimeMillis() - startTime - this.retyTime > tryTime) {
return Boolean.FALSE;
}
// 睡眠重试时间,避免太频繁的重试(单位毫秒)
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(this.retyTime));
}
}
@Override
public boolean tryLock() {
final long startTime = System.currentTimeMillis();
// 是否死循环获取锁
boolean forever = this.outTime < 0;
this.outTime = (this.outTime < 0) ? 0:this.outTime;
// 如果没有加锁成功,循环尝试获取锁
while (true) {
// 获取成功 退出
if (lock()) {
return Boolean.TRUE;
}
// 如果不是必须获取到锁,超过了获取锁的最长时间,退出
if (!forever && System.currentTimeMillis() - startTime - this.retyTime > this.outTime) {
return Boolean.FALSE;
}
// 睡眠重试时间,避免太频繁的重试(单位毫秒)
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(this.retyTime));
}
}
@SuppressWarnings("unchecked")
@Override
public boolean unLock() {
Long result = (Long) redisTemplate.execute(RedisScript.of(RELEASE_LOCK_SCRIPT, Long.class),
Collections.singletonList(lockKey), lockValue);
return result != null && result > 0;
}
}
/**
* 工厂类
*/
@Component
public class RedisFactory {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* @param lockKey
* @return
*/
public RedisLock getLock(String lockKey) {
return new RedisLock(lockKey, redisTemplate);
}
/**
* @param lockKey
* @param expireTime 锁过期时间
* @return
*/
public RedisLock getLock(String lockKey, long expireTime) {
return new RedisLock(lockKey, expireTime, redisTemplate);
}
/**
*
* @param lockKey
* @param expireTime 过期时间
* @param outTime 请求超时阻塞时间( 小于0会一直阻塞慎用)
* @param retyTime 重试时间间隔
* @return
*/
public RedisLock getLock(String lockKey, long expireTime, long outTime, long retyTime) {
return new RedisLock(lockKey,expireTime,outTime,retyTime,redisTemplate);
}
测试
创建两个定时任务,争取同一个锁,每次只能有一个能执行成功。
测试在一个服务里,实际使用是不同应用服务之间的锁争夺。比如集群和微服务。
@Component
@Slf4j
public class ResendTack {
@Autowired
RedisFactory redisFactory;
/**
* 5秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void locTest1() throws InterruptedException {
log.info("locTest1尝试获取锁");
Lock lock = redisFactory.getLock("lock_key");
boolean isLock = lock.lock();
if (!isLock) {
log.info("locTest1获取锁失败");
return;
}
try {
log.info("locTest1获取锁成功");
TimeUnit.SECONDS.sleep(5);
} finally {
log.info("locTest1释放锁");
lock.unLock();
}
}
/**
* 5秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void locTest2() throws InterruptedException {
log.info("locTest2尝试获取锁");
Lock lock = redisFactory.getLock("lock_key");
boolean isLock = lock.lock();
if (!isLock) {
log.info("locTest2获取锁失败");
return;
}
try {
log.info("locTest2获取锁成功");
TimeUnit.SECONDS.sleep(5);
} finally {
log.info("locTest2释放锁");
lock.unLock();
}
}
}
结合springAOP
天天用spring开发,肯定要利用spring的优点简化我们的代码的。结合springAOP编程,提供方法级别的注解来使用
注解
/**
* 分布式锁注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WithLock {
/**
* redis锁的key前缀 如果为空,则默认为类名+方法名+参数
*
* @return
*/
String key() default "";
/**
* 锁过期时间默认30秒(单位毫秒)
*
* @return
*/
long expireTime() default 30000L;
/**
* 请求锁的超时时间,默认0秒(单位:毫秒)
*
* @return
*/
long outTimet() default 0L;
/**
* 重试间隔时间,默认0.5秒(单位:毫秒)
*
* @return
*/
long retryTime() default 500L;
/**
* 获取锁失败时候的失败提示
*
* @return
*/
String errorMessage() default "";
}
aop解析
@Aspect
@Component
@Slf4j
public class LockAspect {
@Autowired
RedisFactory redisFactory;
@Pointcut("@annotation(alun.cn.lock.annotion.WithLock)")
public void lockPoint() {
}
@Around(value = "@annotation(withLock)", argNames = "pjp, withLock")
public Object around(ProceedingJoinPoint pjp, WithLock withLock) throws Throwable {
Class clazz = pjp.getTarget().getClass();
String methodName = pjp.getSignature().getName();
MethodSignature ms = (MethodSignature) pjp.getSignature();
// 请求的方法参数名称
ParameterNameDiscoverer d = new DefaultParameterNameDiscoverer();
// 得到参数名列表
//分布式锁的key
StringBuilder key = new StringBuilder("lock:");
if (withLock.key().equals("")) {
// key=类名+方法名+参数名
key.append(clazz.getName() + ":").append(methodName + ":");
String[] parameterNames = d.getParameterNames(ms.getMethod());
if (parameterNames.length > 0) {
key.append(handleParams(Arrays.asList(parameterNames)));
}
} else {
key.append(withLock.key());
}
RedisLock redisLock = redisFactory.getLock(key.toString(), withLock.expireTime(), withLock.outTimet(),
withLock.retryTime());
Object result = null;
boolean lockSuccess = false;
try {
// 获得锁
lockSuccess = redisLock.tryLock();
if (lockSuccess) {
try {
//执行方法
result = pjp.proceed();
} catch (Exception e) {
log.error("执行业务发生错误,class={},method={},args={}", clazz.getName(), methodName, pjp.getArgs());
throw e;
}
} else {
if (!withLock.errorMessage().equals("")) {
log.info(withLock.errorMessage());
}
}
} catch (Exception e) {
throw new LockException("分布式锁获取时异常", e);
} finally {
if (lockSuccess && !redisLock.unLock()) {
log.error("释放分布式锁失败,class : {},method : {}, key : {}", clazz.getName(), methodName, key);
}
}
return result;
}
private String handleParams(List<String> list) {
final StringBuffer str = new StringBuffer();
list.forEach(param -> str.append(param).append("_"));
str.deleteCharAt(str.length() - 1);
return str.toString();
}
}
使用
@WithLock(key = "text1",outTimet = 5000,errorMessage = "locTest2请求超时")
public void locTest2() throws InterruptedException {
log.info("locTest2获取锁成功");
TimeUnit.SECONDS.sleep(5);
}
注意
-
这个锁必须要设置一个过期时间。否则当一个客户端获取锁成功之后,还没来得及解锁就宕机了,那么它就会一直持有这个锁,而其它客户端永远无法获得这个锁了。
-
获取锁的操作和释放锁的操作必须是原子性,所以使用Lua脚本来实现。
-
value必须要有且唯一,它保证了一个客户端释放的锁必须是自己持有的那个锁。
-
不推荐redis集群模式下使用,redis采用的是主从异步复制的策略,这会导致短时间内主库和从库数据短暂的不一致。官方redisson工具已经实现上述所有功能以及针对集群Redlock算法,完全可以拿来直接使用。
-
可能会出现业务时间大于锁过期时间(使用守护线程监控过期时间、定时任务事实监控),参考redisson待优化
-
多个客户端锁的争夺是随机的,优化方向公平锁