前面几篇文章都是介绍的java juc lock包单机锁,但是目前很多应用都是分布式的(不同jvm进程),单机锁已经不能满足应用的需求了。
网上有关分布式锁的文章很多有基于memcached,redis,zookeeper,但感觉直接拿到线上使用,供公司内部使用还是差点火候,于是就一行代码搞定分布式锁文章就诞生了。
现在几乎每个应用都会用redis,我们就以redis为例使用分布式锁:
先看在springboot项目中使用方式:
lockTemplate.doBiz(LockedCallback lockedCallback,String scene,String uniqueId,String key,Long expireSecond)
这样一来你只需要在回调类lockedCallback写你的业务逻辑,极大地简化了使用分布式锁的使用,能让你更加专注你的业务逻辑,集中管理所有的锁,而不是项目中每个人一套(而且并不是所有人都能用对)。
下面是我做的测试:
@RestController
@RequestMapping(path="/monitor")
public class MonitorController {
@Autowired
private LockTemplate lockTemplate;
private ExecutorService pool = Executors.newFixedThreadPool(10);
/** * @description * @param * @return * @date: 2018/6/12 下午5:18 * @author:yzMa */
@GetMapping("/lock/go")
public ApiResult
受到网上前辈的影响,使用redis.set(key,val,expire,nx)命令加锁,使用lua脚本执行解锁,至于为什么自行百度下(多笔操作这个失败了,那个超时了,加锁了释放锁失败了如何处理等等),我也是踩过很多坑,分析过很多次才有了这个线上版本。
分布式锁模板类
@Slf4j
@Component
public class LockTemplate {
@Resource(name = "luaRedisDistributeLock")
private DistributeLock distributeLock;
public R doBiz(LockedCallback 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 doBiz(LockedCallback 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() {
@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() {
@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 callback();
}
redisTemplate配置类
@Bean("cardRedisTemplate")
public RedisTemplate cardRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate 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分布式锁的正确姿势
考虑好这些问题之后再开始动手写。