前言:
随着互联网的发展,单体架构所存在的问题也一一爆了出来,如部署成本高,迭代速度慢,不易于扩展等问题,微服务架构也油然而生,微服务的出现,并不是为了替代原先单体架构,而是为了解决单体架构出现的相关问题;微服务并不是为了取代某一种程序架构,而是它更适合于某种业务场景或更好地解决某种问题。
然而微服务的出现也会带来一些相关的问题如:
- 分布式问题更加复杂化:因为本来分布式问题就存在,比如分布式锁,分布式事务,数据一致性等问题,随着服务的细化,自然就让分布式问题更加复杂化;
- 问题排查增加难度:微服务很多时,如果出现问题,需要明确的定位,比起单体定位问题更加难啦,但可以借助追踪工具和日志分析工具进行辅助;
- 整体项目质量管控更难:从性能方面,多服务交互需消耗网络IO;单个服务挂了,如果处理不好可能导致系统雪崩;一般需要做熔断、隔离、限流等相关防护等;
我们今天主要讲一下微服务带来的分布式问题分布式锁的实现及使用场景
在我们日常工作中终会有一些这样的场景,多个进程必须以互斥的方式独占共享资源,这时用分布式锁是最直接有效的。
分布式锁的实现方式:
- redis 实现分布式锁
- ZK 实现分布式锁
- Mysql 实现分布式锁
其实就是选用一个公共的存储中间件
来进行状态的监听;
分布式锁的条件:
- 互斥:在分布式高并发的条件下,我们最需要保证,
同一时刻只能有一个线程获得锁
,这是最基本的一点。 - 防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,
避免造成死锁
的情况。 - 性能:对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。所以在锁的设计时,需要考虑两点。(1)
锁的颗粒度要尽量小
(2)锁的范围尽量要小
- 重入:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。
redis 实现分布式锁:
引入Maven依赖,在pom.xml文件加入下面的代码:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
我们通过Redis命令setnx
实现:
SETNX key value //当且仅当不存在的时候才会添加
以上命令容易造成死锁,所以需要设置有效期:
SETEX key seconds value //将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。
如果 key 已经存在,setex命令将覆写旧值。
setex是一个原子性(atomic)操作,关联值和设置生存时间两个动作会在同一时间内完成。不用担心完成设置值设置time时失败的情况
我们来走一下Java代码通过Jedis来实现:
加锁:
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁(未实现重入)
*
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, int expireTime) {
try (Jedis jedis = initJedis()) {
lockKey = "lock:" + lockKey;
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
} catch (Exception e) {
log.error("Redis tryLock is fail", e);
return false;
}
}
- 当前没有锁(key不存在)的时候,那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端
return true
; - 已有锁存在,不做任何操作
return false
;
解锁:
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁 通过lua保证原子性
*
* @return 是否释放成功
*/
public boolean unLock(String lockKey,String requestId) {
try (Jedis jedis = initJedis()) {
lockKey = "lock:" + lockKey;
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
} catch (Exception e) {
log.error("Redis tryLock is fail", e);
return false;
}
}
那段lua脚本的意思是:首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)
- 在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令
- lua可以确保上述操作是原子性的
业务实现:
/**
* 分布式加锁逻辑
*
* @param key
* @return
*/
private boolean saveCacheLock(String key) {
String requestId = UUID.randomUUID().toString();
try {
if (!redisClient.tryLock(key, requestId, expireMsecs)) {
log.info("获取锁失败,key = {} requestId = {}", key, requestId);
return false;
}
//todo 业务代码
} finally {
redisClient.unLock(key, requestId);
}
return true;
}
当然我们也可以选用redisson
不需要我们重复造轮子,源码分析就算啦,感兴趣的大家可以搜一下了解一下,底层也是通过lua保证原子性的;
简单给大家写一下引入步骤:Github上有详细教程 直接搜redisson就ok
导入pom依赖:
<!--Maven-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.4</version>
</dependency>
放一个工具类直接使用就可以:
import java.util.concurrent.TimeUnit;
import com.caisebei.aspect.lock.springaspect.lock.DistributedLocker;
import org.redisson.api.RLock;
/**
* redis分布式锁帮助类
* @author caisebei
*
*/
public class RedissLockUtil {
private static DistributedLocker redissLock;
public static void setLocker(DistributedLocker locker) {
redissLock = locker;
}
/**
* 加锁
* @param lockKey
* @return
*/
public static RLock lock(String lockKey) {
return redissLock.lock(lockKey);
}
/**
* 释放锁
* @param lockKey
*/
public static void unlock(String lockKey) {
redissLock.unlock(lockKey);
}
/**
* 释放锁
* @param lock
*/
public static void unlock(RLock lock) {
redissLock.unlock(lock);
}
/**
* 带超时的锁
* @param lockKey
* @param timeout 超时时间 单位:秒
*/
public static RLock lock(String lockKey, int timeout) {
return redissLock.lock(lockKey, timeout);
}
/**
* 带超时的锁
* @param lockKey
* @param unit 时间单位
* @param timeout 超时时间
*/
public static RLock lock(String lockKey, TimeUnit unit ,int timeout) {
return redissLock.lock(lockKey, unit, timeout);
}
/**
* 尝试获取锁
* @param lockKey
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
return redissLock.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
}
/**
* 尝试获取锁
* @param lockKey
* @param unit 时间单位
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
return redissLock.tryLock(lockKey, unit, waitTime, leaseTime);
}
}
需要配置Redisson环境
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
复制代码
ok!一个简单的Redis分布式锁就实现了,大家也可以根据自己的需求去做优化,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;
最后:
最近我整理了整套《JAVA核心知识点总结》,说实话 ,作为一名Java程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手等公司的Offer!
点击这里进Java架构资源交流群 ,找管理员获取哦-!