Java分佈式鎖(一行代碼搞定)
前面幾篇文章都是介紹的java juc lock包單機鎖,但是目前很多應用都是分佈式的(不同jvm進程),單機鎖已經不能滿足應用的需求了。
網上有關分佈式鎖的文章很多有基於memcached,redis,zookeeper,但感覺直接拿到線上使用,供公司內部使用還是差點火候,於是就一行代碼搞定分佈式鎖文章就誕生了。
現在幾乎每個應用都會用redis,我們就以redis爲例使用分佈式鎖:
先看在springboot項目中使用方式:
lockTemplate.doBiz(LockedCallback<R> lockedCallback,String scene,String uniqueId,String key,Long expireSecond)
這樣一來你只需要在回調類lockedCallback寫你的業務邏輯,極大地簡化了使用分佈式鎖的使用,能讓你更加專注你的業務邏輯,集中管理所有的鎖,而不是項目中每個人一套(而且並不是所有人都能用對)。
下面是我做的測試:
@RestController
@RequestMapping(path="/monitor")
public class MonitorController {
@Autowired
private LockTemplate<String> lockTemplate;
private ExecutorService pool = Executors.newFixedThreadPool(10);
/**
* @description
* @param
* @return
* @date: 2018/6/12 下午5:18
* @author:yzMa
*/
@GetMapping("/lock/go")
public ApiResult<Object> lockTest(){
for (int i = 0; i < 6; i++) {
pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return doSomething();
}
});
}
return ApiResult.buildSuccess(null);
}
/**
* @description 競態條件
* @param
* @return
* @date: 2018/6/12 下午6:15
* @author:yzMa
*/
private String doSomething(){
return lockTemplate.doBiz(new LockedCallback<String>() {
@Override
public String callback() {
try {
System.out.println(Thread.currentThread().getName()+" 成功獲取到了鎖開始執行邏輯");
Thread.sleep(4000);
System.out.println(Thread.currentThread().getName()+" 成功獲取到了鎖開始執行邏輯結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "aa";
}
},"testLock-scene","aaaa-uniqueId","aaaa",5L);
}
}
受到網上前輩的影響,使用redis.set(key,val,expire,nx)命令加鎖,使用lua腳本執行解鎖,至於爲什麼自行百度下(多筆操作這個失敗了,那個超時了,加鎖了釋放鎖失敗瞭如何處理等等),我也是踩過很多坑,分析過很多次纔有了這個線上版本。
分佈式鎖模板類
@Slf4j
@Component
public class LockTemplate<R> {
@Resource(name = "luaRedisDistributeLock")
private DistributeLock distributeLock;
public <R> R doBiz(LockedCallback<R> lockedCallback,String scene,String uniqueId,String key,Long expireSecond){
if(StringUtils.isBlank(uniqueId)){
log.info("|doBiz no uniqueId use serialize lock (變爲分佈式串行鎖)");
uniqueId = key+System.currentTimeMillis();
}
boolean acquiredSuccess = false;
try{
acquiredSuccess = distributeLock.tryLock(key, uniqueId, expireSecond);
if(!acquiredSuccess){
log.info("|doBiz acquire lock fail scene={}, key={},uniqueId={},expireSecond={}",scene,key,uniqueId,expireSecond);
throw new BusinessException(ApiResult.ACQUIRE_LOCK_FAIL,ApiResult.ACQUIRE_LOCK_FAIL_MSG);
}
log.info("|doBiz acquire lock success scene={}, key={},uniqueId={},expireSecond={}",scene,key,uniqueId,expireSecond);
return (R)lockedCallback.callback();
}catch (Exception e){
if(! (e instanceof BusinessException)){
log.error("|doBiz|lockedCallback not BusinessException, scene={},key={},uniqueId={},expireSecond={} exception e:",scene,key,uniqueId,expireSecond,e);
}
throw e;
}finally {
if(acquiredSuccess){
distributeLock.unlock(key,uniqueId);
log.info("|doBiz release lock success scene={}, key={},uniqueId={},expireSecond={}",scene,key,uniqueId,expireSecond);
}
}
}
// 分佈式鎖模板,大鎖 沒有併發
public <R> R doBiz(LockedCallback<R> lockedCallback,String scene,String key,Long expireSecond){
return doBiz(lockedCallback,scene,"",key,expireSecond);
}
}
lua分佈式鎖實現類
@Component("luaRedisDistributeLock")
public class LuaRedisDistributeLock implements DistributeLock{
private String unlockLua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate stringRedisTemplate;
private JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
class ExpirationSub extends Expiration{
long expirationTime;
TimeUnit timeUnit;
public ExpirationSub(long expirationTime,TimeUnit timeUnit){
super(expirationTime,timeUnit);
}
}
@Override
public void lock(String key, String uniqueId, Long expireTime) {
throw new RuntimeException("lua redis not support for now");
}
@Override
public boolean tryLock(String key,String uniqueId, Long expireSeconds) {
Assert.notNull(key,"redis key 不能爲空");
Assert.notNull(uniqueId,"uniqueId 不能爲空");
Assert.notNull(expireSeconds,"expireTime 不能爲空");
Assert.isTrue(expireSeconds > 0 && expireSeconds <= 10 ,"鎖的過期時間範圍介於(0,10]秒");
Boolean lockedSuccess = stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
return redisConnection.set(key.getBytes(), val, expirationSub, RedisStringCommands.SetOption.SET_IF_ABSENT);//老版本
}
});
if(lockedSuccess){
return true;
}
return false;
}
public void unlock(String key, String uniqueId) {
//使用Lua腳本刪除Redis中匹配value的key,可以避免由於方法執行時間過長而redis鎖自動過期失效的時候誤刪其他線程的鎖
//spring自帶的執行腳本方法中,集羣模式直接拋出不支持執行腳本的異常,所以只能拿到原redis的connection來執行腳本
stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
Object nativeConnection = redisConnection.getNativeConnection();
//集羣模式和單機模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行
Long luaResult = 0L;
if (nativeConnection instanceof JedisCluster) {
luaResult = (Long)((JedisCluster) nativeConnection).eval(unlockLua, Collections.singletonList(key), Collections.singletonList(uniqueId));
}
if(nativeConnection instanceof Jedis){
luaResult = (Long)((Jedis)nativeConnection).eval(unlockLua, Collections.singletonList(key), Collections.singletonList(uniqueId));
}
return luaResult != 0;
}
});
}
}
分佈式鎖接口類
public interface DistributeLock {
void lock(String key,String uniqueId,Long expireTime);
boolean tryLock(String key,String uniqueId,Long expireTime);
void unlock(String key,String uniqueId);
}
業務回調類
public interface LockedCallback<R> {
R callback();
}
redisTemplate配置類
@Bean("cardRedisTemplate")
public <String,V> RedisTemplate<String,V> cardRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String,V> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//定義key序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();//Long類型會出現異常信息;需要我們上面的自定義key生成策略,一般沒必要
template.setKeySerializer(stringRedisSerializer);
//定義value的序列化方式
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
在你寫分佈式鎖的時候最好先看下這篇文章redis分佈式鎖的正確姿勢
考慮好這些問題之後再開始動手寫。