2023年4月

前言

我们日常开发过程,会有一些定时任务的代码来统计一些系统运行数据,但是我们应用有需要部署多个实例,传统的通过配置文件来控制定时任务是否启动又太过繁琐,而且还经常出错,导致一些异常数据的产生

网上有很多分布式锁的实现方案,基于redis、zk、等有很多,但是我的就是一个用了mysql和mongo的小应用,不准备引入其他三方中间件来解决这个问题,撸一个简单的分布式锁来解决定时任务并发执行的问题,加锁操作的原子性和防死锁也都要支持,这里我使用mongodb写了AllInOne的工具类

All in one Code

先上代码

@Component
@Slf4j
public class MongoDBLock {

    private static final int DEFAULT_LOCK_TIMEOUT = 30;//锁的默认超时时间,单位秒

    private MongoTemplate mongoTemplate;
    private int lockTimeout;

    public MongoDBLock(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
        this.lockTimeout = DEFAULT_LOCK_TIMEOUT;
    }

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey 锁的key
     * @return true:获取锁成功,false:获取锁失败
     */
    private boolean acquireLock(String lockKey) {
        LockDocument document = new LockDocument();
        document.setId(lockKey);
        document.setExpireAt(Instant.ofEpochMilli(Instant.now().toEpochMilli() + lockTimeout * 1000));
        try {
            mongoTemplate.insert(document);
            return true;
        } catch (Exception e) {

        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param lockKey 锁的key
     */
    private void releaseLock(String lockKey) {
        Query query = new Query(Criteria.where("key").is(lockKey));
        mongoTemplate.remove(query, LockDocument.class);
        log.info("程序执行成功,释放分布式锁,lockKey:{}",lockKey);
    }

    /**
     * 分布式锁入口方法,参数lockName为锁的名称,lockKey为需要加锁的key,执行完成后自动释放锁
     *
     * @param lockKey
     * @param task
     * @param <T>
     * @throws Exception
     */
    public <T> void executeWithLock(String lockKey, ITask<T> task) throws Exception {
        boolean locked = acquireLock(lockKey);
        if (locked) {
            log.info("获取分布式锁成功,lockKey:{}",lockKey);
            try {
                task.execute();
            } finally {
                releaseLock(lockKey);
            }
        } else {
            log.warn("获取分布式锁失败,lockKey:{}", lockKey);
            throw new AppException("获取分布式锁失败!");
        }
    }

    @Data
    @Document(collection = "lock_collection")
    static class LockDocument {
        @Id
        private String id;
        @Indexed(expireAfterSeconds = DEFAULT_LOCK_TIMEOUT)
        private Instant expireAt;
    }

    @FunctionalInterface
    public interface ITask<T> {
        T execute() throws Exception;
    }
}

调用示例

    @Resource
    MongoDBLock mongoDBLock;

    mongoDBLock.executeWithLock("key", () -> {
        // do some thing
        return null;
    });

原理

  • 使用key作为主键,利用mongodb的insert原子性保障LockDocument不会重复插入
  • LockDocument中expireAt字段利用的mongodb索引过期机制,解决死锁问题,这里设置超时时间是30秒,并在执行完成之后会主动释放锁

本文介绍基于
Python

ArcPy
模块,对大量
长时间序列
栅格遥感影像文件的
每一个像元
进行
多时序平均值
的求取。

在遥感应用中,我们经常需要对
某一景
遥感影像中的
全部像元的像素值
进行平均值求取——这一操作很好实现,基于
ArcMap
软件或者简单的
Python
代码就可以实现;但有时候,我们会需要结合同一地区、
不同时相

多景
遥感影像,求取
每一个像元

全部时相
中像素值的平均值——这一需求的实现较之前者就有些麻烦,本文对此加以介绍。

首先,我们来明确一下本文的具体需求。现有一个存储有大量
.tif
格式遥感影像的文件夹,其中每一个遥感影像的文件名中都包含有该图像的成像时间,如下图所示。且其中除了
.tif
格式的遥感影像文件外,还具有其它格式的文件。

我们希望,对于
同一年
成像的遥感影像进行逐像元平均值的求取。例如,上图中具有
2001
年第
185
天成像、第
193
天成像、第
201
天成像……等等遥感影像
8
幅,每一幅都是这一年不同时间在同一空间位置的成像;同时,还有
2005
年不同时间成像的遥感影像
9
幅。我们希望,首先将
2001
年成像的
8
幅遥感影像加以逐像元平均值的求取,即求取
每一个像元
在这
8景图像
中像素值的平均;随后再对
2005
年成像的
9
幅遥感影像加以逐像元平均值的求取,以此类推。

明确了需求后,我们就可以开始具体的操作。首先,本文所需用到的代码如下。

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 16 10:48:37 2022

@author: fkxxgis
"""

import arcpy
from arcpy.sa import *

tif_file_path="E:/LST/Data/MODIS/05_Resample/"
average_file_path="E:/LST/Data/MODIS/06_Average/"
arcpy.env.workspace=tif_file_path

tif_file_name=arcpy.ListRasters("*","tif")
tif_file_year=tif_file_name[0][0:4]
one_year_tif_list=[]
sum_pic=0

for tif_file in tif_file_name:
    if tif_file[0:4]==tif_file_year:
        one_year_tif_list.append(tif_file)
        tif_file_temp=tif_file
        if tif_file==tif_file_name[len(tif_file_name)-1]:
            pic_num=len(one_year_tif_list)
            for tif_file_new in one_year_tif_list:
                sum_pic=sum_pic+Raster(tif_file_new)
            (sum_pic/pic_num).save(average_file_path+tif_file_year+"_Ave.tif")
    else:
        pic_num=len(one_year_tif_list)
        for tif_file_new in one_year_tif_list:
            sum_pic=sum_pic+Raster(tif_file_new)
        (sum_pic/pic_num).save(average_file_path+tif_file_year+"_Ave.tif")
        one_year_tif_list=[]
        sum_pic=0
        one_year_tif_list.append(tif_file)
        tif_file_year=tif_file[0:4]

其中,
tif_file_path
是原有计算平均值前遥感图像的保存路径,
average_file_path
是我们新生成的求取平均值后遥感影像的保存路径,也就是结果保存路径。

在这里,和我们前期的博客
Python ArcPy批量拼接长时间序列栅格图像
类似,需要首先在
资源管理器中
,将
tif_file_path
路径下的各文件以“
名称
”排序的方式进行排序;随后,利用
arcpy.ListRasters()
函数,获取路径下原有的全部
.tif
格式的图像文件,并截取第一个文件的部分文件名,从而获取其成像时间的具体年份。

接下来,遍历
tif_file_path
路径下全部
.tif
格式图像文件。其中,我们通过一个简单的判断语句
if tif_file[0:4]==tif_file_year:
,来确定某一年的遥感影像是否已经读取完毕——如果已经读取完毕,例如假如
2001
年成像的8幅遥感影像都已经遍历过了,那么就对这8景遥感影像加以逐像元的平均值求取,并开始对下一个年份(即
2005
年)成像的遥感影像继续加以计算;如果还没有读取完毕,例如假如
2001
年成像的8幅遥感影像目前仅遍历到了第5幅,那么就不求平均值,继续往下遍历,直到遍历完
2001
年成像的8幅遥感影像。

这里相信大家也看到了为什么我们要在前期先将文件夹中的文件按照“
名称
”排序——是为了保证
同一年成像的所有遥感影像都排列在一起
,遍历时只要遇到一个
新的年份
,程序就知道
上一个年份
的所有图像都已经遍历完毕了,就可以将
上一个年份
的所有栅格图像加以平均值求取。

在这里,逐像元的平均值求取其实也非常简单——我们对每一个像元分别执行以下操作:首先将该像元在当前年份里所有
遥感影像的像素值
相加,随后除以这一年份的
遥感影像的数量
,得到的就是该像元在这一年中
像素值的平均值

最后,通过
if tif_file==tif_file_name[len(tif_file_name)-1]:
这个判断,来确认是否目前已经遍历到文件夹中的
最后一个
图像文件。如果是的话,就需要将
当前成像年份
的所有图像进行平均值的求取,并宣告代码完成运行。


IDLE (Python GUI)
中运行代码。代码运行完毕后,我们看一下结果文件夹。可以看到,其中的图像已经是按照成像时间,分别完成平均值求取后的结果了。

在最后,还需要说明一点——用以上代码来求取长时间序列遥感影像的像元平均值,对于任意一个像元,只要该像元在
任意一个时相
的图像中是无效值(即为
NoData
),那么该像元在最终求出的平均值
结果图中
,像素值也将会是无效值
NoData
。针对这一问题的解决,我们将在下一篇博客中介绍。

最为一名越过菜鸟之后的开发,需要做接口开发。下面做一个纯粹的接口编程的实例demo,仅仅是一个webapi接口的抽象。

下面是代码接口,AbsEFWork是webapi,BaseEntityFramework是一个接口库。

先介绍一下webapi的实现,代码是从底层往上层写的,阅读代码的习惯应该是自上向下。

 public class ProductController : CustomController<Product>{public ProductController(IEFCoreService<Product> efCoreService) : base(efCoreService)
{
}
}

控制器代码很简单的实现了CustomController,数据载体是Product。

usingBaseEntityFramework.Implementations;usingBaseEntityFramework.Implementations.Entitys;usingBaseEntityFramework.IService;usingMicrosoft.EntityFrameworkCore;namespaceBaseEntityFramework
{
public classProgram
{
public static void Main(string[] args)
{
var builder =WebApplication.CreateBuilder(args);//Add services to the container. builder.Services.AddControllers();
builder.Services.AddDbContext
<ProductDbContext>(options => options.UseInMemoryDatabase("memorydb"));//Learn more about configuring Swagger/OpenAPI athttps://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped
<IEFCoreService<Product>, EFCoreProductService>();var app =builder.Build();//Configure the HTTP request pipeline. if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();


app.MapControllers();

app.Run();
}
}
}

Program启动程序需要实现IEFCoreService的注入,以及ProductDbContext 的内存实现。

这样就可以启动一个swagger

对于product数据存储的具体实现,实体类product和dbcontext必须要自己去实现它。

 public classProduct:IEntity
{
[Key]
public int Id { get; set; }public string Name { get; set; }public string Description { get; set; }

[Column(TypeName
= "decimal(28,16)")]public decimal Price { get; set; }
}
usingBaseEntityFramework.Implementations.Entitys;usingMicrosoft.EntityFrameworkCore;namespaceBaseEntityFramework.Implementations
{
public classProductDbContext:DbContext
{
public ProductDbContext(DbContextOptions<ProductDbContext> dbContextOptions):base(dbContextOptions)
{

}
public DbSet<Product> Products { get; set; }
}
}

查看上面的控制器代码,有注入IEFCoreService<Product>的业务代码,对于接口肯定是需要一个实现,这里可以再次封装一个抽象的基类来(温馨提示:重复的代码,必须优化),我这里暂时没做处理。

usingBaseEntityFramework.Implementations.Entitys;usingBaseEntityFramework.IService;usingBaseEntityFramework.Models;usingMicrosoft.EntityFrameworkCore;usingSystem.Linq.Expressions;namespaceBaseEntityFramework.Implementations
{
public class EFCoreProductService : IEFCoreService<Product>{private readonlyProductDbContext _dbContext;publicEFCoreProductService(ProductDbContext productDbContext)
{
_dbContext
=productDbContext;
}
public async Task<bool>Add(Product entity)
{
_dbContext.Products.Add(entity);
var result = await_dbContext.SaveChangesAsync();return result != 0;
}
public Task<bool>Delete(Product entity)
{
throw newNotImplementedException();
}
public async Task<IEnumerable<Product>>GetAll()
{
var result =await_dbContext.Products.ToListAsync();returnresult;
}
public Task<Product> GetEntity(Expression<Func<Product, bool>>expression)
{
throw newNotImplementedException();
}
public async Task<IReadOnlyCollection<Product>> GetList(Expression<Func<Product, bool>>expression)
{
var result = await_dbContext.Products.Where(expression).ToListAsync();returnresult.AsReadOnly();
}
public Task<PageResult<Product>> GetPageResult<Req>(PageInput<Req> pagInput) where Req : new()
{
throw newNotImplementedException();
}
public Task<bool>Update(Product entity)
{
throw newNotImplementedException();
}
}
}

上面的代码很简单易懂,最大的好处就是可以复用。实体类和 dbcontext越多这个简简单单的结构就越是有用。

BaseEntityFramework的核心逻辑就是把业务代码做了抽象,做了一个统一的模板,不管 是从那方便说都只有好处。而且作为开发只关心自己的业务代码这一块。

 public interface IEFCoreService<T> whereT:IEntity
{
Task
<bool>Add(T entity) ;
Task
<bool>Delete(T entity);
Task
<bool>Update(T entity);
Task
<IReadOnlyCollection<T>> GetList(Expression<Func<T,bool>>expression) ;
Task
<PageResult<T>> GetPageResult<Req>(PageInput<Req> pagInput) where Req:new();
Task
<T> GetEntity(Expression<Func<T, bool>>expression);
Task
<IEnumerable<T>>GetAll();
}

以上的实例只是一个简单的demo,项目中需要做框架的话这或许是一个开始,需要做的远远不止这些。

源代码:

liuzhixin405/AbsEFWork (github.com)

1、在线服务器导出requirement.txt

pip freeze > requirement.txt

该文件生成完毕后,需要做些修改,去掉不需要的库,否则下载的时候会出错。

2、下载whl文件 -> packages

pip download -r requirement.txt -d packages
pip download -r requirement.txt -d packages -i https://pypi.tuna.tsinghua.edu.cn/simple/

下载完成后packages文件夹包含所有的whl文件。

3、离线服务器ubuntu20.04  查看是否包含python pip

这个版本的服务器已经安装python3.8.5,但是没有安装pip,从而导致安装whl文件失败。

没有python的则需要自行安装。

4、离线安装pip

安装pip的话需要提前安装setuptools,所以需要下载两个包。

------下载

setuptools下载地址:https://pypi.org/project/setuptools/

pip下载地址:https://pypi.org/project/pip/

需要注意对应的python版本

我下载的是setuptools==45.2.0    pip==23.1 他们都是要求python>=3.7,最开始我下载的是setuptools==67.6.1,但是出现了问题,最后我选择了45版本。

-----解压安装setuptools

tar -xvzf setuptools-45.2.0.tar.gz
cd setuptools
-45.2.0python3 setup.py install

出现了问题 No module named 'distutils.core'

百度了一圈发现是少了插件,离线的话又需要重新下载安装

python3-distutils地址:https://pkgs.org/search/?q=python3-distutils    有时候需要人机检验,搜索完成后,选择服务器版本与python版本,大致对应即可

点击进入详细地址:https://ubuntu.pkgs.org/20.04/ubuntu-main-amd64/python3-distutils_3.8.2-1ubuntu1_all.deb.html

查看需要的依赖

还需要python3-lib2to3这个,因为python已经有了,直接点击进去也是选择对应版本即可。

插件下载地址是在download部分

直接打开http://archive.ubuntu.com/ubuntu/pool/main/p/python3-stdlib-extensions/python3-distutils_3.8.2-1ubuntu1_all.deb 这个链接开始下载python3-distutils_3.8.2

同理打开http://archive.ubuntu.com/ubuntu/pool/main/p/python3-stdlib-extensions/python3-lib2to3_3.8.2-1ubuntu1_all.deb 下载python3-lib2to3_3.8.2

下载完成后上传至同一目录

----安装setuptools依赖

sudo dpkg -i *.deb

安装完成后再安装setuptools,安装完成后import 看看有没有问题。

-----解压安装pip

setuptools安装成功后就可以安装pip了

tar -xvzf pip-23.1.tar.gz
cd pip
-23.1python3 setup.py install

安装完成后执行pip list 查看成功与否

------安装所有whl文件

当离线服务器pip准备就绪后就可以安装了

pip install --no-index --find-links=packages -r requirement.txt 

执行完毕后,pip list查看,如果没问题则转移成功。

有一位老师想要把官网上有关数字化的文章全部下载下来,于是找到我,使用python来达到目的

首先先查看了文章的网址

获取了网页的源代码发现一个问题,源代码里面没有url,这里的话就需要用到抓包了,因为很明显这里显示的内容是进行了一个请求,所以只能通过抓包先拿到请求的url从而获得每一篇文章对应的url,获取到了之后使用python全部下载到了一个文本文件中

这时候我们就拿到了所有文章的链接,接下来写函数实现获取网页源代码,这里用到了python爬虫常用的BeautifulSoup处理源代码很方便以下是实现的代码:

defhtml(url):
head
={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67","cookie": "Hm_lvt_af43f8d0f4624bbf72abe037042ebff4=1640837022; __gads=ID=a34c31647ad9e765-22ab388e9bd6009c:T=1637739267:S=ALNI_MYCjel4B8u2HShqgmXs8VNhk1NFuw; __utmc=66375729; __utmz=66375729.1663684462.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __gpi=UID=000004c822cf58b2:T=1649774466:RT=1663684463:S=ALNI_Ma3kL14WadtyLP-_lSQquhy_w85ag; __utma=66375729.1148601284.1603116839.1663684462.1663687392.2; .Cnblogs.AspNetCore.Cookies=CfDJ8NfDHj8mnYFAmPyhfXwJojexiKc4NcOPoFywr0vQbiMK4dqoay5vz8olTO_g9ZwQB7LGND5BBPtP2AT24aKeO4CP01olhQxu4EsHxzPVjGiKFlwdzRRDSWcwUr12xGxR89b_HFIQnmL9u9FgqjF6CI8canpEYxvgxZlNjSlBxDcWOzuMTVqozYVTanS-vAUSOZvdUz8T2XVahf8CQIZp6i3JzSkaaGUrXzEAEYMnyPOm5UnDjXcxAW00qwVmfLNW9XO_ITD7GVLrOg-gt7NFWHE29L9ejbNjMLECBdvHspokli6M78tCC5gmdvetlWl-ifnG5PpL7vNNFGYVofGfAZvn27iOXHTdHlEizWiD83icbe9URBCBk4pMi4OSRhDl4Sf9XASm7XKY7PnrAZTMz8pvm0ngsMVaqPfCyPZ5Djz1QvKgQX3OVFpIvUGpiH3orBfr9f6YmA7PB-T62tb45AZ3DB8ADTM4QcahO6lnjjSEyBVSUwtR21Vxl0RsguWdHJJfNq5C5YMp4QS0BfjvpL-OvdszY7Vy6o2B5VCo3Jic; .CNBlogsCookie=71474A3A63B98D6DA483CA38404D82454FB23891EE5F8CC0F5490642339788071575E9E95E785BF883C1E6A639CD61AC99F33702EF6E82F51D55D16AD9EBD615D26B40C1224701F927D6CD4F67B7375C7CC713BD; _ga_3Q0DVSGN10=GS1.1.1663687371.1.1.1663687557.1.0.0; Hm_lvt_866c9be12d4a814454792b1fd0fed295=1662692547,1663250719,1663417166,1663687558; Hm_lpvt_866c9be12d4a814454792b1fd0fed295=1663687558; _ga=GA1.2.1148601284.1603116839; _gid=GA1.2.444836177.1663687558; __utmt=1; __utmb=66375729.11.10.1663687392"}
response
= requests.get(url, headers=head) #获取网页信息 response.encoding = 'utf-8' #html = response.text #所有内容 content =response.content.decode()#匹配文章标题 pattern2 = r'"pageTitle" content="(.*?)">'match2=re.search(pattern2, content)#标题 bt = match2.group(1)
soup
= BeautifulSoup(content,'html.parser')#内容 nr=soup.get_text()
write(bt,nr)

伪造一个header的头,因为学校官网设置的有简易的反爬机制,所以需要伪装成正常的浏览器访问,写一个简单的正则匹配文章的标题作为txt的文件名

现在拿到了标题和文章内容就可以写入文本了

创建文本文件并写入内容代码:

defwrite(bt,nr):
with open(r
'C:\Users\13777\Desktop\猜猜看\1\\'+bt+'.txt','w',encoding='utf-8') as f:
f.write(nr)
with open(r
'C:\Users\13777\Desktop\猜猜看\1\\'+bt+'.txt','r',encoding='utf-8') as f:
lines
=f.readlines()#切片方法,从第4行开始,到倒数第2行结束 new_lines = lines[67:-1]
with open(r
'C:\Users\13777\Desktop\猜猜看\1\\'+bt+'.txt','w',encoding='utf-8') as f:
f.writelines(new_lines)
print('yes')
with open(r
'C:\Users\13777\Desktop\猜猜看\url.txt') as t:for line int.readlines():
url
=line.strip()
html(url)

这里遇到一个问题就是经过BeautifulSoup处理后的内容前面有一段是没有任何作用的文本,于是写入文本再进行切片把前面没有用处的文本去掉,剩下的都是文章的内容

最终实现的效果: