分布式锁设计

一、基础概念

1.1什么是分布式锁?

要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问,主要应用在分布式场景中。

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

1.2 分布式锁的使用场景

线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!

因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

有这样一个情境,线程A和线程B都共享某个变量X。

如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。

如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

业务场景:

1.多个进程需要同时对数据库进行DML(插入,更新,删除)
2.重复提交场景,可以用分布式锁解决,即控制幂等

二、分布式锁的实现

分布式锁的三种方案

基于数据库实现分布式锁, 基于缓存(redis,memcached,tair)实现分布式锁, 基于Zookeeper实现分布式锁。

在分析这几种实现方案之前我们先来想一下,我们需要的分布式锁应该是怎么样的?(这里以方法锁为例,资源锁同理)

可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

  • 这把锁要是一把可重入锁(避免死锁)
  • 这把锁最好是一把阻塞锁(未获取到锁的线程有对应的等待机制,如放入阻塞队列或者抛弃不执行)
  • 有高可用的获取锁和释放锁功能
  • 获取锁和释放锁的性能要好

2.1基于数据库实现分布式锁

实现思路:

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。

数据表案例如下:
分布式锁设计_第1张图片
当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,如果其他进程发现已经有这条纪录时,则获取锁失败,想要释放锁的时候就删除这条记录。

存在的问题:

1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

当然,我们也可以有其他方式解决上面的问题。

  • 数据库是单点?搞两个数据库,进行主从同步。一旦挂掉快速切换到备库上。
  • 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  • 非阻塞的?搞一个while循环,直到insert成功再返回成功。即使用自旋锁的思想。
  • 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

基于数据库排他锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁,此时其他线程对该行读写会被阻塞

三种方案的比较
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)
缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库

@see 分布式锁的几种实现方式 https://www.cnblogs.com/austinspark-jessylu/p/8043726.html

2.2 分布式锁的实现(Redis)

分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。首先我们要搭建一个Redis服务器,用Redis服务器来存储锁信息。

在实现的时候要注意的几个关键点:

1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;

2、同一时刻只能有一个线程获取到锁。

几个要用到的redis命令:

setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。

get(key):获得key对应的value值,若不存在则返回null。

getset(key, value):先获取key对应的value值,若不存在则返回null,然后将旧的value更新为新的value。

expire(key, seconds):设置key-value的有效期为seconds秒。

ttl:以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。

返回值:
当 key 不存在时,返回 -2 。
当 key 存在但没有设置剩余生存时间时,返回 -1 。
否则,以秒为单位,返回 key 的剩余生存时间。

2.2.1 Redis分布式锁的基本流程

Redis分布式锁的基本流程并不难理解,但要想写得尽善尽美,也并不是那么容易。在这里,我们需要先了解分布式锁实现的三个核心要素:

1.加锁

最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。
比如想要给一种商品的秒杀活动加锁,可以给key命名为 “lock_sale_商品ID” 。
而value设置成什么呢?我们可以姑且设置成1。加锁的伪代码如下:

setnx(key,1)

当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;
当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败,进行自旋或者直接返回。

2.解锁

有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。
释放锁的最简单方式是执行del指令,伪代码如下:

del(key)

释放锁之后,其他线程就可以继续执行setnx命令来获得锁。

3.锁超时

锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。

所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。

setnx不支持超时参数,所以需要额外的指令,伪代码如下:
expire(key, 30)

综合起来,我们分布式锁实现的第一版伪代码如下:

if(setnx(key,1== 1{
    expire(key,30try {

        do something ......

    } finally {

        del(key)

    }
}

2.2.2 Redis分布式锁的升级版

上面的伪代码中,存在着三个致命问题:

  1. setnx和expire的非原子性

产生场景:设想一个极端场景,当某线程执行setnx,成功得到了锁:
setnx刚执行成功,还未来得及执行expire指令,节点1 Duang的一声挂掉了。
这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。

解决方案:setnx指令本身是不支持传入超时时间的,幸好Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:

set(key,130,NX)

这样就可以取代setnx指令。

实际生产中项目使用可参考:

private boolean setRedisLockKey(String key, long expireInMillis) {
        try {
            String result = (String)this.redisTemplate.execute(new RedisCallback<String>() {
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    JedisCommands commands = (JedisCommands)connection.getNativeConnection();
                    String lockValue = UUID.randomUUID().toString();
                    RedisDistLock.this.localCacheOflockValue.set(lockValue);
                    return commands.set(key, lockValue, "NX", "PX", expireInMillis);
                }
            });
            return StringUtils.isNotEmpty(result);
        } catch (Exception var5) {
            LOG.error("set redis lock key : {} failed", key, var5);
            return false;
        }
    }
  1. del 导致误删

产生场景:又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。

随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。这样就会导致并发问题。

解决方案:可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

加锁:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)

解锁:

if(threadId .equals(redisClient.get(key)){
    del(key)
}

判断和释放锁保证原子性

但是,这样做又隐含了一个新的问题,判断和释放锁是两个独立操作,不是原子性。
我们都是追求极致的程序员,所以这一块要用Lua脚本来实现:
String luaScript = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”;

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

这样一来,验证和删除过程就是原子操作了。

实际生产中项目使用可参考:

 public boolean unlock(String key) {
        try {
            final List<String> keys = Collections.singletonList(key);
            final List<String> args = Collections.singletonList(this.localCacheOflockValue.get());
            Long result = (Long)this.redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    if (nativeConnection instanceof JedisCluster) {
                        return (Long)((JedisCluster)nativeConnection).eval(RedisDistLock.UNLOCK_LUA, keys, args);
                    } else {
                        return nativeConnection instanceof Jedis ? (Long)((Jedis)nativeConnection).eval(RedisDistLock.UNLOCK_LUA, keys, args) : 0L;
                    }
                }
            });
            return result != null && result > 0L;
        } catch (Exception var5) {
            LOG.error("unlock: {} failed", key, var5);
            return false;
        }
    }

其中线程的值使用ThreadLocal来实现的。

private ThreadLocal<String> localCacheOflockValue = new ThreadLocal();
  1. 锁延期

产生场景:还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。即A的超时时间快过了,但A还是没有执行完。

解决思路:
1)把锁的超时时间设置得比较长,那么如果出现获取锁的节点岩机,那么其他节点很难获取到锁。故这种方法不可取。
2)获取锁成功的线程,开启一个守护线程(线程执行完后,守护线程会关掉),当锁的快要过期时,如果还没有执行玩,就给快要过期的锁“续航。

解决方案:我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

代码可参考:

1.加锁成功后,开启一个守护线程。
分布式锁设计_第2张图片
分布式锁设计_第3张图片

2.PostponeTask延时逻辑
分布式锁设计_第4张图片分布式锁设计_第5张图片

使用缓存实现分布式锁的优点:

1.集群部署的,可以避免单点问题

2.读写性能高

缺点:

1.获取失败的进程没有等待机制,但在某些场景反而是它的优点,如控制幂等

2.实现逻辑较为复杂。

2.3 Zookeeper实现分布式锁

参考我的另一个博文:Zookeeper原理分析 https://blog.csdn.net/sinat_34814635/article/details/79237309

2.4 Redisson实现分布式锁

Redisson是redis官方出的分布式锁工具。

它作为工具帮我实现了上面的加锁和解锁的原子性,并且自动开启线程为分布式锁续命。

但没有解决主从节点下异步同步数据导致锁丢失问题。

基本使用方式如下:

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient client = Redisson.create(config);
RLock lock = client.getLock("abcde");
lock.lock();
try {
    ...
} finally {
    lock.unlock();
}

lock() 方法加锁成功 默认过期时间 30 秒, 并且支持 “看门狗” 续时功能

1.实现原理

分布式锁设计_第6张图片
Redisson加锁成功后回开启定义work线程每隔分布式锁的1/3时间去判断是非还持有,如果持有则延迟至分布式锁加锁的时间。

2.加锁流程

<T> Future<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command){
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call('exists', KEYS[1]) == 0) then " +	
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +	
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +	
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +	
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

流程图如下:
分布式锁设计_第7张图片
上边一段lua脚本可以看到redis获取锁的逻辑:
假设key:abcde,Lock的id为123456789 线程为thread1 ,有效期为 10

  1. 如果不存在 key:abcde,设置 abcde里面一个键值对 123456789:thread1 值为 1,并设置有效期 10
  2. 如果存在abcde并且vulue为123456789:thread1,将123456789:thread1 的值加 1
  3. 如果存在abcde但vulue不为123456789:thread1,说明抢锁失败则返回
    过期时间

3.锁续时

分布式锁设计_第8张图片

4.锁释放

public void unlock() {
    Boolean opStatus= commandExecutor.evalWrite(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                    "end;" +
                    "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                        "return nil;" +
                    "end; " +
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                    "if (counter > 0) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                        "return 0; " +
                    "else " +
                        "redis.call('del', KEYS[1]); " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; "+
                    "end; " +
                    "return nil;",
                    Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(Thread.currentThread().getId()));
    if (opStatus == null) {
        throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                + id + " thread-id: " + Thread.currentThread().getId());
    }
    if (opStatus) {
        cancelExpirationRenewal();
    }
}

解锁流程如下:
分布式锁设计_第9张图片

5.Redlock

不可否认, Redisson 设计的分布式锁真的很 NB, 但是还是没有解决 主从节点下异步同步数据导致锁丢失问题。

比如获取到锁的线程1对应的redis master宕机了,redis开始选择从节点并将从节点升级为master,线程2加锁时,可能没有读到线程1的加锁结果而导致重复加锁。

所以 Redis 作者 Antirez 推出 红锁算法, 这个算法的精髓就是: 没有从节点, 如果部署多台 Redis, 各实例之间相互独立, 不存在主从复制或者其他集群协调机制

分布式锁设计_第10张图片

当然, 对于 Redlock 算法不是没有质疑声, 大家可以去 Redis 官网查看Martin Kleppmann 与 Redis 作者Antirez 的辩论。

故在实际生产环境,因为master服务宕机而导致的异步延迟可能性比较小,如果要完全解决主从问题,则推荐使用Zookeeper。

版权声明:本文为CSDN博主「Java后端架构猛猛」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_67645544/article/details/123902011

2.5 Zookeeper和redis实现分布式锁对比

分布式锁设计_第11张图片
zk:
1.zk满足的cap中cp,需要考虑集群中写的性能
2.添加和删除节点性能低的原因是要保证zk集群的强一致性。
redis:
1.redis满足的cap中ap,需要考虑集群中的一致性,
2.需要考虑超时,原子性,误删原因

三、使用案例

1.多台服务器同时下载ftp中的excel文件,并解析插入数据库。

所遇技术难点:

服务器启动且每隔10分钟就会下载ftp上的资源,当有多台服务器时,每个服务器很有可能会下载到ftp上的同一个资源,在插入数据库时就会出现重复的数据。
那么如何保证数据的不重复且能保证分布式环境下的性能呢?

解决办法:
采用分布式锁来保证数据的唯一性。通过redis来存储锁的标志(即状态)

为何选择redis?
redis的环境地址独立于服务器,即使有多台服务器集群,但每个服务器读写的都是同一单位的redis,就如一个全局变量一样被每台服务器所共享。

1.1 锁标志
1:已加锁
0:未加锁

1.2前提条件
进入流程前提条件是已从ftp中获得了此资源

分布式锁设计_第12张图片

部分流程解释:
1.获取锁标志为0时出现的情况:是在执行插入等业务失败后会进入异常,从而不会执行删除ftp上A资源的方法,而定时任务每10分钟就会执行一次,这样就可以实现下载失败后继续重试。
2.continue:是指下载ftp的其他资源如资源B,这样既保证了数据的唯一性又兼容了分布式环境下的系统性能。
使用文件名作为key值,不同文件名可以同时并发执行不会因为锁而阻塞。提高了并发性能
参考代码如下:

@Scheduled(fixedDelay = 10 * 60 * 1000, initialDelay = 5 * 1000)
    private void updateDB() {
        FtpUtil ftpUtil = new FtpUtil(ftpConfig.getIp(), ftpConfig.getPort(), ftpConfig.getUserName(), ftpConfig.getUserPwd());

        // 从ftp服务器获取excel文件
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
        String year = sdf.format(new Date());

        //路径为当前年+上周自然周
        List fileList = new LinkedList<>();
        fileList.add(FtpInfo.MODULE_AREAEFFICIENCY);
        fileList.add(FtpInfo.MODULE_PEOPLE_EFFICIENCY);
        fileList.add(FtpInfo.MODULE_COMMONDITY_STRUCTURE);
        fileList.add(FtpInfo.MODULE_PROTOTYPE_MANAGE);
        fileList.add(FtpInfo.MODULE_DIRECTIVITY_BRAND);
        fileList.add(FtpInfo.MODULE_STANDARD);
        fileList.add(FtpInfo.MODULE_STANDARD_STORE);
        fileList.add(FtpInfo.MODULE_STANDARD_BRAND);

        for (String fileShortName : fileList) {
            //excel下载后的路径
            String filePath = moduleExcelFacade.downloadExcel(ftpUtil, fileShortName);
            if (StringUtil.isEmpty(filePath)) {
                continue;
            }
            File file=new File(filePath);
            // 获取锁标志,保证同一时间只能最多一个服务器操作
            final String fileMark = cacheDatabaseUtil.get(fileShortName);
            if (fileMark != null && fileMark.equals(CacheDatabaseUtil.DB_MARK_DO)) {
                continue;
            }

            // 修改锁的标志
            cacheDatabaseUtil.put(fileShortName, CacheDatabaseUtil.DB_MARK_DO);

            //备份路径
            String substring = filePath.substring(filePath.lastIndexOf("-") + 1, filePath.lastIndexOf("."));
            String Path = year + "/" + substring;
            //文件名全部
            String fileName = fileShortName + "-" + substring + ".xlsx";
            try {
                if (filePath.indexOf("样机管理") != -1) {
                    final List prototypeManageList = ImportExcel.getPrototypeManageList(filePath);
                    prototypeManageService.insert(prototypeManageList);
                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("标准值") != -1 && filePath.indexOf("门店标准值") == -1 && filePath.indexOf("品牌标准值") == -1) {
                    final List standardList = ImportExcel.getStandardLinkedList(filePath);
                    standardService.insert(standardList);
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("指向性品牌") != -1) {
                    final List directivityBrandLinkedList = ImportExcel.getDirectivityBrandLinkedList(filePath);
                    directivityBrandService.insert(directivityBrandLinkedList);

                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("坪效") != -1) {
                    final List areaEfficiencyLinkedList = ImportExcel.getAreaEfficiencyLinkedList(filePath);
                    areaEfficiencyService.insert(areaEfficiencyLinkedList);


                    //下载成功,插入成功后向ftp进行备份

                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("人效") != -1) {
                    final List peopleEfficiencyLinkedList = ImportExcel.getPeopleEfficiencyLinkedList(filePath);
                    peopleEfficiencyService.insert(peopleEfficiencyLinkedList);
                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("商品结构") != -1) {
                    final List commodityStructureLinkedList = ImportExcel.getCommodityStructureLinkedList(filePath);
                    commodityStructureService.insert(commodityStructureLinkedList);

                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("门店标准值") != -1) {
                    final List standardStoreLinkedList = ImportExcel.getStandardStoreLinkedList(filePath);
                    standardStoreService.insert(standardStoreLinkedList);
                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                } else if (filePath.indexOf("品牌标准值") != -1) {
                    final List standardBrandLinkedList = ImportExcel.getStandardBrandLinkedList(filePath);
                    standardBrandService.insert(standardBrandLinkedList);
                    //下载成功,插入成功后向ftp进行备份
                    boolean flag = ftpUtil.copyFile(FtpInfo.MODULE_DIRECTORY, fileName, FtpInfo.MODULE_DIRECTORY, Path);
                    if (flag) {
                        logger.info("{}备份成功", fileName);
                    } else {
                        logger.info("{}备份失败", fileName);
                    }
                    if (ftpUtil.deleteFile(FtpInfo.MODULE_DIRECTORY, null, fileName)) {
                        logger.info("ftp上{}目录下的文件:{}删除成功", FtpInfo.MODULE_DIRECTORY, fileName);
                    } else {
                        logger.info("ftp上{}目录下的文件:{}删除失败", FtpInfo.MODULE_DIRECTORY, fileName);
                    }
                }

                //删除之前要把所有文件流关闭,才能删除成功
                boolean result = file.delete();
                if (result) {
                    logger.info("本地目录下的文件:{}删除成功", fileName);
                } else {
                    System.gc();
                    boolean delete = file.delete();
                    if (delete){
                        logger.info("本地目录下的文件:{}删除成功", fileName);
                    }else {
                        logger.info("本地目录下的文件:{}删除失败", fileName);
                    }
                }
            } catch (Exception e) {
                logger.error("解析excel失败,文件名:{},异常信息:{}", filePath, e.toString());
                continue;
            } finally {
                cacheDatabaseUtil.put(fileShortName, CacheDatabaseUtil.DB_MARK_UNDO);
            }
        }
    }

参考资料

1.漫画:什么是分布式锁?http://www.ibloger.net/article/3205.html
2.漫画:如何用Zookeeper实现分布式锁? http://www.ibloger.net/article/3206.html
3.redis分布式锁的作用及实现https://blog.csdn.net/ntotl/article/details/80368355
4.Redis 命令参考 http://redisdoc.com/
5.Redis分布式锁如何解决锁超时问题的?https://blog.csdn.net/weixin_39685762/article/details/111103020

你可能感兴趣的:(分布式,分布式)