一.为什么需要分布式锁
目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。基于 CAP理论,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。单机版部署涉及到单进程多线程模型,正好可以采用JVMsynchronized和Lock本地锁实现,而在分布式的场景中涉及到的是多进程多线程模型,JVM本地锁不能在多进程中共享,故分布式锁诞生。
二.分布式锁的实现方式
1.redis分布式锁
2.基于数据库的分布式锁
3.基于zookeeper的分布式锁
本文探讨基于redis实现的分布式锁
三.一个分布式锁应该具有哪些特性?
1.独占性:任何时刻有且只有一个线程可以获取到锁
2.高可用:在redis集群下,不能因为某一个节点挂掉而导致获取锁和释放锁失败的情况、
3.防死锁:防止死锁,必须要有超时机制或者撤销操作
4.不乱抢:加锁或者释放锁只能操作自己的锁,不能妨碍到他人
5.可重入:同一个节点同一个线程获取到该锁后,它也可以再次获得这个锁
四.实战案例
1.基于setnx实现的分布式锁
@Override
public String sale() {
String key = "sale";
String keynx = "lvzan";
String resultMsg = "";
String uuid = UUID.randomUUID().toString().replace("-", "") + ":" + Thread.currentThread().getId();
//Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, uuid);
// 此处不能使用if,因为采用递归的方式容易导致OOM,我们可以采用JVM本地锁中的CAS自旋锁的思想
// if (!flag){
// //暂停20毫秒后递归调用
// try {
// TimeUnit.MILLISECONDS.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// sale();
// } else {
while (!redisTemplate.opsForValue().setIfAbsent(keynx, uuid, 30L, TimeUnit.SECONDS)){
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
// 1.查询库存信息
String sale = redisTemplate.opsForValue().get(key);
// 2.判断库存是否足够
int saleNum = sale == null ? 0 : Integer.valueOf(sale);
// 3.扣减库存
if (saleNum > 0){
redisTemplate.opsForValue().set(key, String.valueOf(-- saleNum));
resultMsg = "成功卖出一个商品,还剩下:" + saleNum;
System.out.println("resultMsg = " + resultMsg);
}else {
resultMsg = "商品卖完了";
}
}finally {
// 该操作不是原子性的,所以我们采用lua脚本代替
// if (redisTemplate.opsForValue().get(keynx).equalsIgnoreCase(uuid)){
// redisTemplate.delete(keynx);
// }
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
String luaScript =
"if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(keynx), uuid);
}
return resultMsg+"\t"+"服务端口号:"+port;
}
2.基于hash实现分布式锁
基于setnx实现的分布式锁存在着不可重入的缺点,而采用hash实现正好可以解决此问题,利用的是每重入一次就+1,释放锁时-1即可
package com.roar.edr.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class RedisDistributedLock implements Lock {
private RedisTemplate<String, String> redisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, String uuidValue, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue;
this.expireTime = 30L;
}
@Override
public void lock() {
tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
// 实现加锁功能
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1L){
this.expireTime = unit.toSeconds(time);
}
// lua脚本
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
while (Boolean.FALSE.equals(redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))){
TimeUnit.MILLISECONDS.sleep(50);
}
return true;
}
// 实现解锁功能
@Override
public void unlock() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null) throw new RuntimeException("This lock doesn't EXIST");
}
@Override
public Condition newCondition() {
return null;
}
}
不过这还不算很完美,我们也要确保此锁,在执行业务代码期间不过期失效,因此我们需要另外开启一个线程为此锁续期