分布式系统中一般都会存在资源竞争,java存在的锁和并发控制只能保证同一个jvm下的资源分配。分布式系统就要使用其他方式来保证资源的按顺序获取。早期系统比较常见的就是通过数据库中行级锁加乐观锁的方式实现,但是这种方式效率比较低,主要是数据库是硬盘级别的io读取速率,效率不高。在redis并广泛使用以后,redis就是分布式锁更合理的解决方案。
可以自己编码通过redis来实现分布式锁,实际在生成环境不一定要用这个方式。自我实现的好处是对redis实现分布式锁的原理可以有比较好的认识,同时也能比较好的理解什么事分布式锁,同时认识的不同的实现方式存在什么样的问题。
先看一个基础实现例子
public boolean lock(String key, V v, int expireTime){
int retry = 0;
//获取锁失败最多尝试10次
while (retry < failRetryTimes){
//获取锁
Boolean result = redis.setNx(key, v, expireTime);
if (result){
return true;
}
try {
//获取锁失败间隔一段时间重试
TimeUnit.MILLISECONDS.sleep(sleepInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public boolean unlock(String key){
return redis.delete(key);
}
public static void main(String[] args) {
Integer productId = 324324;
RedisLock<Integer> redisLock = new RedisLock<Integer>();
redisLock.lock(productId+"", productId, 1000);
}
}
这个简单例子基本实现了分布式所有核心功能。其实就是锁的获取和锁的释放。
但是这个实现存在不少问题:
可以做几个基本的修改
private static volatile int count = 0;
public boolean lock(String key, V v, int expireTime){
int retry = 0;
//获取锁失败最多尝试10次
while (retry < failRetryTimes){
//1.先获取锁,如果是当前线程已经持有,则直接返回
//2.防止后面设置锁超时,其实是设置成功,而网络超时导致客户端返回失败,所以获取锁之前需要查询一下
V value = redis.get(key);
//如果当前锁存在,并且属于当前线程持有,则锁计数+1,直接返回
if (null != value && value.equals(v)){
count ++;
return true;
}
//如果锁已经被持有了,那需要等待锁的释放
if (value == null || count <= 0){
//获取锁
Boolean result = redis.setNx(key, v, expireTime);
if (result){
count = 1;
return true;
}
}
try {
//获取锁失败间隔一段时间重试
TimeUnit.MILLISECONDS.sleep(sleepInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public boolean unlock(String key, String requestId){
String value = redis.get(key);
if (Strings.isNullOrEmpty(value)){
count = 0;
return true;
}
//判断当前锁的持有者是否是当前线程,如果是的话释放锁,不是的话返回false
if (value.equals(requestId)){
if (count > 1){
count -- ;
return true;
}
boolean delete = redis.delete(key);
if (delete){
count = 0;
}
return delete;
}
return false;
}
但是这个实现本身也有问题,就是先取出来判断造成了整个操作不是原子级的。
可以采用lua脚本的方式保证整个操作的原子级
public final int failRetryTimes = 10;
public final int waitIntervalInMS = 100;
public boolean lock(String key, String t, int expireTemp) {
int retry = 0;
boolean result;
try {
while (retry < failRetryTimes) {
retry++;
result = redisTemplate.opsForValue().setIfAbsent(key, t, expireTemp, TimeUnit.SECONDS);
if (result) {
return true;
}
TimeUnit.MILLISECONDS.sleep(waitIntervalInMS);
}
} catch (Exception e) {
log.error(e.getMessage());
}
return false;
}
public void unLock(String key, String value) {
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
redisTemplate.execute((RedisCallback<Boolean>) redisConnection ->
redisConnection.eval(script.getBytes(),
ReturnType.BOOLEAN, 1, key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)));
}
在实际的使用过程中可以试用Redission框架来使用他帮我们实现好的分布式锁解决方案。而且这个解决方案支持集群式架构的redis。
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-data-22artifactId>
<version>3.12.2version>
dependency>
singleServerConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: null
subscriptionsPerConnection: 5
clientName: null
address: "redis://192.168.0.128:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 0
#在最新版本中dns的检查操作会直接报错 所以我直接注释掉了
#dnsMonitoring: false
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: ! {
}
transportMode : "NIO"
@Bean
RedissonClient redissonClient() throws IOException {
Config config = new Config();
//单机模式 依次设置redis地址和密码
config.useSingleServer().
setAddress("redis://192.168.0.128:6379");
config.setCodec(new StringCodec());
return Redisson.create(config);
}
主要要注意redission针对不同的redis部署方式有不同的装配方式
public void lockDemo() throws InterruptedException {
System.out.println("我要开始处理东西了");
RLock rLock = redissonClient.getLock("red-key");
if (rLock.tryLock(5, 20, TimeUnit.SECONDS)) {
log.info("我在这里处理了点事情" + Thread.currentThread().getName());
}
try {
Thread.sleep(10);
System.out.println("我处理完了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (rLock.isLocked()) {
rLock.unlock();
}
}
}
可以写个测试方法测试下
@Test
public void lockTest() throws InterruptedException {
for (int i = 0; i < 10; i++) {
log.info("开始-----------------");
new Thread(() -> {
try {
demoService.lockDemo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(1000);
}