2019年2月17日 By mikel 分类: 架构设计

随着现在数据量的不断增加,很多大数量的问题随之而来,就得需要我们想办法解决,我找了一些问题并首先思考,然后找到方法,在这里记录一下,未来有需要的同学可以拿走去用。 1. 在海量日志数据里,提取某天访问

来源: 海量数据处理方法整理记录 – 黄青石 – 博客园

随着现在数据量的不断增加,很多大数量的问题随之而来,就得需要我们想办法解决,我找了一些问题并首先思考,然后找到方法,在这里记录一下,未来有需要的同学可以拿走去用。

1. 在海量日志数据里,提取某天访问量最多的IP。

一般处理海量的思路都是分治处理,就是现将数据进行拆分,然后进行处理,排序等。这个例子也不例外,IPV4的地址一共32位,最大值为2^32也就是总数大约4G左右,如果放到内存里边,以目前的内存容量也是可以处理的,但是咱们可以为自己设置一些条件,比如目前没有那么多内存。

a) 首先分治,将这个文件按照IP的HASH分成1024份(如果想要均匀的分的算法需要使用一致性Hash算法),这样每个文件大约4M左右并且存放到磁盘上去。

b) 构建一个需要以IP为Key,出现次数为Value的TreeMap。读取每个文件,将IP和出现次数放入有序的TreeMap。

c) 这样就可以得到出现次数最多的IP,前N个出现次数多的IP都可以获取到了。

这种问题一般是TOP K的问题,思路都可以按照这样的思路去解决。当然这种场景比较合适的就是Map Reduce莫属了。另外,关于TOP K的这种排序的话可以采用最小堆排序(即根节点是最小的),它的时间复杂度为n*mlogm,n即为一共多少数据,m为取出前m个数据。关于这种结构不知道的同学可以进行谷歌搜索。分治的作用就是为了减少使用系统的资源,比如系统内容。

2. 上个问题是统计重复出现的个数,那么如何统计不重复的个数。比如:有个电话本,里边记录的电话号码都是8位数字,统计电话本里边有多少电话号码?这个里边肯定也是有一些局限的,比如内存限制。再比如再2.5亿整数中找到不重复的整数的个数,当然,内存中不能够存储着2.5亿数据。这种解决的思路一般是位图算法(bitMap)解决。

以电话号码为例:

a)电话号码是8位数字,也就是出现的数字应该为11111111-99999999,总数为99999999,咱们采用位图法(因为最省内存)。

b)一个bit位代表一个数字,那么这些数字共需要99999999个bit,占用内存为 99999999/8/1024/1024约等于11.92M,即如果这个数字所在的位有数据,那么这个bit位就设置为1,否则设置为0。

这样只需要12M的内存就可以统计这些数据了。当然2.5亿整数同理,在内存中所有整数的个数为2^32,一个数对应一个bit,大概需要512M内存就可以了,如果给的内存还不够的话,则需要再次进行拆分。

3. 还有一些与上边类似的,但是不太相同的,因为有重复的数(1、2、2、3、3、4,排好序的数并且偶数个的话,中位数是[2+3]/2=2.5 奇数个的话正好是中间的),比如在5亿int数中找到中位数。这个问题的解决思路其实采用双层桶划分思路。注意一个int占4个Byte,整数的最大位数为32位,那么我们将每个数转换为二进制,然后截取前多少位,要看内存大小。解决思路:

a) 把整数转为二进制数,然后截取前5位,那么总共分出2^5=32个区间,如果分出文件来共分出32个文件,如果内存不够的话,那么再继续截取(比如16位,这里举例)。比如:file_00000, file_00001等。

b) 如果截取完了,所有文件一共32个文件,因为都是二进制,所以文件是按照有序排好的。统计每个文件的个数,然后计算中位数所在的文件里。

c) 如果文件还是比较大,假设文件在最后一个文件,即前边2.5亿,最后一个文件2.5亿,文件名字为file_11111,那么再继续按照上边的方法继续拆分(比如再5位 文件名:file_11111_00000 等),知道内存中可以装下整个文件。

d) 可以装下整个文件下的话再进行排序,排好序之后,找到中间的数就是中位数。

4. 两个文件,各存放50亿条URL,每个URL占64字节。内存限制是4G,找出两个文件中相同的URL。这个问题有一个内存限制,那么肯定需要分治法。

方法一(分治+hash+hashset):

a) 50亿个64Byte= 5G*64Byte = 320G,内存4个G,肯定是不可以的。那么咱们将每个URL进行hash,然后放到1024个文件中,也就是每个文件为320G/1024=320M左右。以hash值作为文件名,第一个文件hash出来的文件命名为(hash[URL]%1024)a1…..a1024,第二个文件hash出来的文件命名为b1…..b1024。

b)1024个文件生成了,那么相同的URL肯定在hash命名文件的后缀中,比如a1 vs b1,这样依次读取文件的内容放入到hashset中,如果存在的话记录并且追加放到文件中。

c)  最后文件中就是所有URL即为相同的URL。

方法二(Bloom Filter布隆过滤器)

a) 先说一下布隆过滤器,主要将需要内容进行hash,然后对应到相应的bit上,即Bit Map位图法,但是这个里边有一个问题就是hash会碰撞,即不同的结果可能会hash成相同的值,这样就会出错。如果可以接受错误率,当然错误率较低,那么可以采用这种方式。4G内存=2^32 * 8 约等于 40亿Byte * 8 大约等于340亿。先遍历第一个文件,然后再遍历第二个,这样会错误率。

5. 有40亿个不重复的unsigned int的整数,没排过序,现在给一个数,如何快速判断这个数是否在这40亿个数当中。这个如果直接放到内存里边的话得需要2^32*4Byte(int 4Byte) = 4G *4 = 16G. 显然内存比较大了。

a) 这个也采用位图法,所需要的内存为  2*32Byte / 8 = 500M 内存,所以仅仅需要500M内存就可以放下这些数字了,然后查找就可以了。

6. 给定一个文件,里面最多含有n个不重复的正整数(也就是说可能含有少于n个不重复正整数),且其中每个数都小于等于n,n=10^7。输出:得到按从小到大升序排列的包含所有输入的整数的列表。条件:最多有大约1MB的内存空间可用,但磁盘空间足够。且要求运行时间在5分钟以下,10秒为最佳结果。

如果采用位图法的话需要为10^7 / 8 /1024/1024 大约等于1.19M,大于题目的1M,显然位图法不太合适,那么咱们考虑一下多路归并排序。

a)  首先将这个文件分批次读取拆分,比如一次读取256K,然后进行memory sort 在内存排序,写到文件中。假如文件大小是10M的大小,则需要循环40次,写入40个文件当中。

b)  然后将文件进行merge sort合并排序,创建一个数组40个长度,依次读取最小的文件,然后找到数组中最小的写入到文件当中,然后继续读取文件并且继续排序,将最小的再次写入文件即可。

6. 有10个文件,每个文件1G,每个文件的每一行都存放的是用户的搜索的关键字,每个文件的搜索的关键字都可能重复。找出热度高的前1000个搜索关键字。(提示分治+hash+trie树+最小堆)

看到这种问题的话,首先得考虑是否机器资源足够使用,如果足够使用的话,就直接加入内存,但是如果不够的话需要考虑分治。解决思路。

a) 将每个文件按关键字进行hash,然后拆分成100个文件,然后每个文件大概100M左右。(分治+hash)。

b) 读取每个小文件,并且将读取的关键字形成Trie树字典树,这样会达到去重的效果。Trie树的插入和查询复杂度是O(k), k为最长字符串的长度。然后建立长度为1000的小根堆,将遍历每个关键字的出现的次数放到小根堆里。

c) 以上一遍就可以得出第一个1G文件的结果,然后按照相同的原理继续以上步骤。

 

总结一下:

如果是大量数据不重复的,而且需要内存占用比较少的需要找出出现的内容的话,适合使用BitMap位图法进行处理。

还有就是一般的TOP K问题,就是找出前多少位的这种,一般内存容量都不是很大,采用的方式是 分治+hash+最小(大)堆排序。当然分布式的适合处理方式为MapReduce处理。

排序可以有很多种,按照不同的方式进行不同的排序,比如快排,最小堆排序,归并排序。如果大文件需要排序,并且严格要求内存的话,分治成小文件,然后采用归并排序很合适。

如果涉及到单词的类型处理的话,需要使用Trie树进行,因为这个非常合适处理,并且复杂度为O(k)。

 

如果有不对的地方,欢迎指正。

海量数据处理方法整理记录 – 黄青石 – 博客园已关闭评论
2019年2月15日 By mikel 分类: 架构设计

在文章开始之前首先要思考的问题是为什么要建立对象池。这和.NET垃圾回收机制有关,正如下面引用所说,内存不是无限的,垃圾回收器最终要回收对象,释放内存。尽管.NET为垃圾回收已经进行了大量优化,例如将

来源: Object Pooling(对象池)实现 – Zhang_Xiang – 博客园

在文章开始之前首先要思考的问题是为什么要建立对象池。这和.NET垃圾回收机制有关,正如下面引用所说,内存不是无限的,垃圾回收器最终要回收对象,释放内存。尽管.NET为垃圾回收已经进行了大量优化,例如将托管堆划分为 3 Generations(代)并设定新建的对象回收的最快,新建的短生命周期对象将进入 Gen 0(新建对象大于或等于 85,000 字节将被看作大对象,直接进入 Gen 2),而 Gen 0 通常情况下分配比较小的内存,因此Gen 0 将回收的非常快。而高频率进行垃圾回收导致 CPU 使用率过高,当 Gen 2 包含大量对象时,回收垃圾也将产生性能问题。

.NET 的垃圾回收器管理应用程序的内存分配和释放。 每当有对象新建时,公共语言运行时都会从托管堆为对象分配内存。 只要托管堆中有地址空间,运行时就会继续为新对象分配空间。 不过,内存并不是无限的。 垃圾回收器最终必须执行垃圾回收来释放一些内存。 垃圾回收器的优化引擎会根据所执行的分配来确定执行回收的最佳时机。 执行回收时,垃圾回收器会在托管堆中检查应用程序不再使用的对象,然后执行必要的操作来回收其内存。参考

构造对象池

.Net Core 在(Base Class Library)基础类型中添加了 ArrayPool,但 ArrayPool 只适用于数组。针对自定义对象,参考MSDN有一个实现,但没有初始化池大小,且从池里取对象的方式比较粗糙,完整的对象池应该包含:

  • 池大小
  • 初始化委托
  • 实例存取方式(FIFO、LIFO 等自定义方式,根据个人需求实现获取实例方式)
  • 获取实例策略

1. 定义对象存取接口,以实现多种存取策略,例如 FIFO、LIFO

/// <summary>
/// 对象存取方式
/// </summary>
public interface IAccessMode<T>
{
    /// <summary>
    /// 租用对象
    /// </summary>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    T Rent();
    
    /// <summary>
    /// 返回实例
    /// </summary>
    /// <param name="item"></param>
    void Return(T item);
}

2. 实现存取策略

FIFO

FIFO通过Queue实现,参考

public sealed class FIFOAccessMode<T> : Queue<T>, IAccessMode<T>
{
    private readonly int _capacity;
    private readonly Func<T> _func;
    private int _count;

    public FIFOAccessMode(int capacity, Func<T> func) : base(capacity)
    {
        _capacity = capacity;
        _func = func;
        InitialQueue();
    }

    public T Rent()
    {
        Interlocked.Increment(ref _count);
        return _capacity < _count ? _func.Invoke() : Dequeue();
    }

    public void Return(T item)
    {
        if (_count > _capacity)
        {
            var disposable = (IDisposable)item;
            disposable.Dispose();
        }
        else
        {
            Enqueue(item);
        }
        Interlocked.Decrement(ref _count);
    }

    private void InitialQueue()
    {
        for (var i = 0; i < _capacity; i++)
        {
            Enqueue(_func.Invoke());
        }
    }
}
LIFO

在LIFO中借助Stack特性实现进栈出栈,因此该策略继承自Stack,参考

public sealed class LIFOAccessModel<T> : Stack<T>, IAccessMode<T>
{
    private readonly int _capacity;
    private readonly Func<T> _func;
    private int _count;

    public LIFOAccessModel(int capacity, Func<T> func) : base(capacity)
    {
        _capacity = capacity;
        _func = func;
        InitialStack();
    }

    public T Rent()
    {
        Interlocked.Increment(ref _count);
        return _capacity < _count ? _func.Invoke() : Pop();
    }

    public void Return(T item)
    {
        if (_count > _capacity)
        {
            var disposable = (IDisposable)item;
            disposable.Dispose();
        }
        else
        {
            Push(item);
        }
        Interlocked.Decrement(ref _count);
    }

    private void InitialStack()
    {
        for (var i = 0; i < _capacity; i++)
        {
            Push(_func.Invoke());
        }
    }
}

注意:以上两个实现都遵循池容量不变原则,但租用的实例可以超过对象池大小,返还时还将检测该实例直接释放还是进入池中。而如何控制池大小和并发将在下面说明。

3.Pool实现

public class Pool<T> : IDisposable where T : IDisposable
{
    private int _capacity;
    private IAccessMode<T> _accessMode;
    private readonly object _locker = new object();
    private readonly Semaphore _semaphore;

    public Pool(AccessModel accessModel, int capacity, Func<T> func)
    {
        _capacity = capacity;
        _semaphore = new Semaphore(capacity, capacity);
        InitialAccessMode(accessModel, capacity, func);
    }

    private void InitialAccessMode(AccessModel accessModel, int capacity, Func<T> func)
    {
        switch (accessModel)
        {
            case AccessModel.FIFO:
                _accessMode = new FIFOAccessMode<T>(capacity, func);
                break;
            case AccessModel.LIFO:
                _accessMode = new LIFOAccessModel<T>(capacity, func);
                break;
            default:
                throw new NotImplementedException();
        }
    }

    public T Rent()
    {
        _semaphore.WaitOne();
        return _accessMode.Rent();
    }

    public void Return(T item)
    {
        _accessMode.Return(item);
        _semaphore.Release();
    }

    public void Dispose()
    {
        if (!typeof(IDisposable).IsAssignableFrom(typeof(T))) return;

        lock (_locker)
        {
            while (_capacity > 0)
            {
                var disposable = (IDisposable)_accessMode.Rent();
                _capacity--;
                disposable.Dispose();
            }

            _semaphore.Dispose();
        }
    }
}

在Pool中如何控制程序池并发,这里我们引入了 Semaphore 以控制并发,这里将严格控制程序池大小,避免内存溢出。

4.使用

Student 类用作测试

public class Student : IDisposable
{
    public string Name { get; set; }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private bool _disposed;

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            Name = null;
             //Free any other managed objects here.
        }

        _disposed = true;
    }
}
public void TestPool()
{
    Func<Student> func = NewStudent;
    var pool = new Pool<Student>(AccessModel.FIFO, 2, func);
    for (var i = 0; i < 3; i++)
    {
        Student temp = pool.Rent();
        //todo:Some operations
        pool.Return(temp);
    }

    Student temp1 = pool.Rent();

    pool.Return(temp1);

    pool.Dispose();
}

public Student NewStudent()
{
    return new Student();
}

总结:至此,一个完整的对象池建立完毕。

Object Pooling(对象池)实现 – Zhang_Xiang – 博客园已关闭评论
2019年2月14日 By mikel 分类: 架构设计

来源: ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立 – NanaseRuri – 博客园

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 jQuery》

 

当此系列文章写完后会在一周内推出修正版。

 

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

 

 

本章内容:对图书馆系统组成的简要分析。以及对域模型以及相应数据库的建立。

知识点:Code First、EF 基本使用方法、ASP.NET Core 使用 EF Core 的配置方法、EF 多对多关系的建立。

 

 

一、对图书馆系统域模型的分析

一个图书馆系统需要有管理员、 学生、书架以及书籍

 

域模型,即用来存储数据的模型。

在此域模型可以用以下结构创建:

 

 

 

 二、项目结构

然后就可以开始建立该项目了:

 

 

 

三、建立域模型

学位枚举:

复制代码
1     public enum Degrees
2     {
3         [Display(Name = "本科生")]
4         CollegeStudent,
5         [Display(Name = "研究生")]
6         Postgraduate,
7         [Display(Name = "博士生")]
8         DoctorateDegree
9     }
复制代码

 

图书借阅状态枚举:

复制代码
 1     public enum BookState
 2     {
 3         /// <summary>
 4         /// 可借阅
 5         /// </summary>
 6         [Display(Name = "正常")]
 7         Normal,
 8 
 9         /// <summary>
10         /// 馆内阅览
11         /// </summary>
12         [Display(Name = "馆内阅览")]
13         Readonly,
14 
15         /// <summary>
16         /// 已借出
17         /// </summary>
18         [Display(Name = "已借出")]
19         Borrowed,
20 
21         /// <summary>
22         /// 被续借
23         /// </summary>
24         [Display(Name = "被续借")]
25         ReBorrowed,
26 
27         /// <summary>
28         /// 被预约
29         /// </summary>
30         [Display(Name = "被预约")]
31         Appointed,
32 
33         [Display(Name = "过期")]
34         Expired
35     }
复制代码

 

该项目准备使用一个数据库存储学生账户信息,另一个则用于存储学生借书信息:

学生账户信息:

复制代码
 1     public class Student : IdentityUser
 2     {
 3         /// <summary>
 4         /// 学号
 5         /// </summary>
 6         [ProtectedPersonalData]
 7         [RegularExpression("[UIA]\\d{9}")]
 8         [Display(Name = "学号")]
 9         public override string UserName { get; set; }
10 
11         [Display(Name = "手机号")]
12         [StringLength(14, MinimumLength = 11)]
13         public override string PhoneNumber { get; set; }
14 
15         [Display(Name = "姓名")]
16         public string Name { get; set; }
17         [Display(Name = "学历")]
18         public Degrees Degree { get; set; }
19         [Display(Name = "最大借书数目")]
20         public int MaxBooksNumber { get; set; }
21     }
复制代码

 

书籍信息:

复制代码
 1     public class Book
 2     {                                
 3         /// <summary>
 4         /// 二维码
 5         /// </summary>
 6         [Key]
 7         [Display(Name = "二维码")]
 8         [Required(ErrorMessage = "未填写二维码")]
 9         public string BarCode { get; set; }
10 
11         public string ISBN { get; set; }
12 
13         /// <summary>
14         /// 书名
15         /// </summary>
16         [Display(Name = "书名")]
17         public string Name { get; set; }
18 
19         /// <summary>
20         /// 取书号
21         /// </summary>
22         [Display(Name = "取书号")]
23         public string FetchBookNumber { get; set; }
24 
25         /// <summary>
26         /// 所在书架
27         /// </summary>
28         public Bookshelf Bookshelf { get; set; }
29 
30         [Display(Name = "书架号")]
31         public int BookshelfId { get; set; }
32 
33         /// <summary>
34         /// 借出时间
35         /// </summary>
36         [Display(Name = "借出时间")]
37         public DateTime? BorrowTime { get; set; }
38 
39         /// <summary>
40         /// 到期时间
41         /// </summary>
42         [Display(Name = "到期时间")]
43         public DateTime? MatureTime { get; set; }
44 
45         /// <summary>
46         /// 预约最晚借书日期
47         /// </summary>
48         [Display(Name = "预约取书时间")]
49         public DateTime? AppointedLatestTime { get; set; }
50 
51         /// <summary>
52         /// 借阅状态
53         /// </summary>
54         [Display(Name = "书籍状态")]
55         public BookState State { get; set; }
56 
57         /// <summary>
58         /// 持有者,指定外键
59         /// </summary>
60         public StudentInfo Keeper { get; set; }
61         [Display(Name = "持有者学号")]
62         public string KeeperId{ get; set; }
63 
64         [Display(Name = "位置")]
65         public string Location { get; set; }
66 
67         [Display(Name = "分类")]
68         public string Sort { get; set; }
69 
70         public ICollection<AppointmentOrLending> Appointments { get; set; }
71     }
复制代码

 

书架信息:

复制代码
 1     public class Bookshelf
 2     {
 3         /// <summary>
 4         /// 书架ID
 5         /// </summary>
 6         [Key]
 7         //不自动增长
 8         [DatabaseGenerated(DatabaseGeneratedOption.None)] 
 9         public int BookshelfId { get; set; }
10 
11         /// <summary>
12         /// 书架的书籍类别
13         /// </summary>
14 
15         [Required]
16         public string Sort { get; set; }               
17         /// <summary>
18         /// 最小取书号
19         /// </summary>
20         [Required]
21         public string MinFetchNumber { get; set; }
22         [Required]
23         public string MaxFetchNumber { get; set; }
24 
25         /// <summary>
26         /// 书架位置
27         /// </summary>
28         [Required]
29         public string Location { get; set; }
30 
31         /// <summary>
32         /// 全部藏书
33         /// </summary>
34         public ICollection<Book> Books { get; set; }
35     }
复制代码

 

 

 

由于一个学生可以借阅多本书籍,一本书籍可被多人预约,因此书籍和学生具有多对多的关系,在此引入中间类:

其中的 AppointingDateTime 用来区分中间类包含的书籍是借阅书籍还是预约书籍:

复制代码
1     public class AppointmentOrLending
2     {
3         public Book Book { get; set; }
4         public string BookId { get; set; }
5         public StudentInfo Student { get; set; }
6         public string StudentId { get; set; }
7         public DateTime? AppointingDateTime { get; set; }
8     }
复制代码

 

学生借书信息:

在 EF 中多对多关系实际上是两个多对一关系。此处 ICollection 的属性成为导航属性,用来提示 EF  StudentInfo 和 AppointmentOrLending 之间存在着多对一的关系。

复制代码
 1     public class StudentInfo
 2     {
 3         [Key]
 4         public string UserName { get; set; }
 5 
 6         [Required]
 7         public string Name { get; set; }
 8 
 9         /// <summary>
10         /// 学位,用来限制借书数目
11         /// </summary>
12         [Required]
13         public Degrees Degree { get; set; }
14 
15         /// <summary>
16         /// 最大借书数目
17         /// </summary>
18         [Required]
19         public int MaxBooksNumber { get; set; }
20 
21         /// <summary>
22         /// 已借图书
23         /// </summary>
24         public ICollection<AppointmentOrLending> KeepingBooks { get; set; }
25 
26         public string AppointingBookBarCode { get; set; }
27 
28         [StringLength(14, MinimumLength = 11)]
29         public string PhoneNumber { get; set; }
30 
31         /// <summary>
32         /// 罚款
33         /// </summary>
34         public decimal Fine { get; set; }               
35     }
复制代码

 

外借/阅览书籍信息:

在约定中,若不指定主键,则 EF 会使用 (类名)+ID 的方式指定或创建主键,在此使用 [Key] 指定主键,使用 [Required] 指定字段为必须,这种可以为属性添加在数据库中的约束或者在视图中的约束的修饰称为 DataAnnotations 。

此处 ICollection 的属性成为导航属性,用来提示 EF  Book 和 AppointmentOrLending 之间存在着多对一的关系。

复制代码
 1     public class Book
 2     {                                
 3         /// <summary>
 4         /// 二维码
 5         /// </summary>
 6         [Key]
 7         [Display(Name = "二维码")]
 8         [Required(ErrorMessage = "未填写二维码")]
 9         public string BarCode { get; set; }
10 
11         public string ISBN { get; set; }
12 
13         /// <summary>
14         /// 书名
15         /// </summary>
16         [Display(Name = "书名")]
17         public string Name { get; set; }
18 
19         /// <summary>
20         /// 取书号
21         /// </summary>
22         [Display(Name = "取书号")]
23         public string FetchBookNumber { get; set; }
24 
25         /// <summary>
26         /// 所在书架
27         /// </summary>
28         public Bookshelf Bookshelf { get; set; }
29 
30         [Display(Name = "书架号")]
31         public int BookshelfId { get; set; }
32 
33         /// <summary>
34         /// 借出时间
35         /// </summary>
36         [Display(Name = "借出时间")]
37         public DateTime? BorrowTime { get; set; }
38 
39         /// <summary>
40         /// 到期时间
41         /// </summary>
42         [Display(Name = "到期时间")]
43         public DateTime? MatureTime { get; set; }
44 
45         /// <summary>
46         /// 预约最晚借书日期
47         /// </summary>
48         [Display(Name = "预约取书时间")]
49         public DateTime? AppointedLatestTime { get; set; }
50 
51         /// <summary>
52         /// 借阅状态
53         /// </summary>
54         [Display(Name = "书籍状态")]
55         public BookState State { get; set; }
56 
57         /// <summary>
58         /// 持有者,指定外键
59         /// </summary>
60         public StudentInfo Keeper { get; set; }
61         [Display(Name = "持有者学号")]
62         public string KeeperId{ get; set; }
63 
64         [Display(Name = "位置")]
65         public string Location { get; set; }
66 
67         [Display(Name = "分类")]
68         public string Sort { get; set; }
69 
70         public ICollection<AppointmentOrLending> Appointments { get; set; }
71     }
复制代码

 

 

 

四、创建 DbContext 

学生账户信息数据库:

1     public class StudentIdentityDbContext:IdentityDbContext<Student>
2     {
3         public StudentIdentityDbContext(DbContextOptions<StudentIdentityDbContext> options) : base(options)
4         {
5         }
6     }

 

借阅信息数据库:

为了使 StudentInfo 类的 UserName 和 Book 的 BarCode 共同作为 AppointmentOrLending 中间类的主键,需覆写 OnModelCreating 方法:

至此 StudentInfo 和 Book 的多对多关系正式确立。

复制代码
 1     public class LendingInfoDbContext:DbContext
 2     {
 3         public LendingInfoDbContext(DbContextOptions<LendingInfoDbContext> options) : base(options)
 4         {
 5         }
 6 
 7         public DbSet<Book> Books { get; set; }
 8         public DbSet<BookDetails> BooksDetail { get; set; }
 9         public DbSet<Bookshelf> Bookshelves { get; set; }
10         public DbSet<RecommendedBook> RecommendedBooks { get; set; }
11         public DbSet<StudentInfo> Students { get; set; }
12         public DbSet<AppointmentOrLending> AppointmentOrLendings { get; set; }
13 
14         protected override void OnModelCreating(ModelBuilder modelBuilder)
15         {
16             base.OnModelCreating(modelBuilder);
17             modelBuilder.Entity<AppointmentOrLending>()
18                 .HasKey(c => new { c.BookId, c.StudentId });
19         }
20     }
复制代码

于是 Book 和 StudentInfo 之间的多对多关系确立完成。

 

 

 

五、根据约定配置数据库,进行依赖注入

在  appsettings.json 中添加数据库连接字符串。

复制代码
 1 {
 2   "ConnectionStrings": {
 3     "LendingInfoDbContext": "Server=(localdb)\\mssqllocaldb;Database=LendingInfoDbContext;Trusted_Connection=True;MultipleActiveResultSets=true",
 4     "StudentIdentityDbContext": "Server=(localdb)\\mssqllocaldb;Database=StudentIdentityDbContext;Trusted_Connection=True;MultipleActiveResultSets=true"
 5   },
 6   "Logging": {
 7     "LogLevel": {
 8       "Default": "Warning"
 9     }
10   },
11   "AllowedHosts": "*"
12 }
复制代码

 

在 Startup.cs 中的 ConfigureServices 方法中对数据库进行配置:

复制代码
1             services.AddDbContext<LendingInfoDbContext>(options =>
2             {
3                 options.UseSqlServer(Configuration.GetConnectionString("LendingInfoDbContext"));
4             });
5             services.AddDbContext<StudentIdentityDbContext>(options =>
6             {
7                 options.UseSqlServer(Configuration.GetConnectionString("StudentIdentityDbContext"));
8             });
复制代码

 

 

 

六、数据库的迁移、创建及更新

然后在 pm控制台 中添加迁移:

添加迁移的语法为 add-migration <迁移类名> -c <具体 DbContext 名>

1       cd LibraryDemo
2       add-migration LendingInfo -c LibraryDemo.Data.LendingInfoDbContext
3       add-migration StudentIdentity -c LibraryDemo.Data.StudentIdentityDbContext

 

运行 add-migration 命令会创建 Migrations 文件夹以及相应的迁移快照:

 

显示的类名为 <创建时间>_<迁移类名>,而实际的类名为 add-migration 后的第一个参数名。

 

在创建迁移时,EF 会自动为我们创建或更新对应 DbContext 的快照,即其中后缀为 Snapshot 的类。其中会包含当前对应的 DbCOntext 的结构,并会以代码保留相应的约束,如 LendingInfoDbContextModelSnapshot 类:

 

生成的迁移类 LendingInfo 和 Account 类则有两个方法—— 用于更新数据库的 Up 方法和用以回溯数据库的 Down 方法,可以在这两个方法或者在快照的 BuildModel 方法中使用 Fluent API 对数据库做进一步的改动,并且通过对 Fluent API 的使用可以使我们的类少用 DataAnnotations 以保证类的整洁。

需要注意的是,生成的迁移类中的 Up 和 Down 方法是根据生成迁移之前的数据库快照生成的,如我在之后为 LendingInfoDbContext 添加 DbSet<RecommendedBook> 时,在以上的基础上运行了 add-migration AddRecommendedBook -c LibraryDemo.Data.LendingInfoDbContext ,生成的 Up 方法只包括添加表 RecommendedBooks 的行为,而 Down 方法只包括删除表 RecommendedBooks 的行为。

 

 

随后在 pm控制台 执行以下创建或更新数据库:

1      update-database -c LibraryDemo.Data.LendingInfoDbContext
2      update-database -c LibraryDemo.Data.StudentIdentityDbContext

 

 

最后在 SQL server对象管理器 中可以看见创建的数据库以及对应的表:

 

 

至此域模型创建工作完成。

 

 

 

 

补充:

使用命令行对数据库进行迁移及更新有两种方式:

1  dotnet ef migrations migrationName -c TargetContext
2  dotnet ef database update -c TargetContext

 

1  add-migration migrationName -c TargetContext
2  update-Database -c TargetContext

 

windows 命令行命令不区分大小写,其中 migrationName 为迁移类名,最好提供有意义的命名;而 TargetContext 为目标 DbContext 类名,需要使用带有命名空间的完全命名。

如果需要删除数据库则使用 drop 方法

drop-database -c TargetContext

 

而为 update 方法指定迁移类则可以回溯数据库。

Update-Database LendingInfoDbContext -TargetMigration:"20181127081115_LendingInfo.cs"
ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立 – NanaseRuri – 博客园已关闭评论
2019年2月14日 By mikel 分类: ASP.NET, 架构设计

基本登录页面以及授权逻辑的建立

来源: ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 – NanaseRuri – 博客园

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 jQuery》

 

当此系列文章写完后会在一周内推出修正版。

 

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

 

 

本章内容:Identity 框架的配置、对账户进行授权的配置、数据库的初始化方法、自定义 TagHelper

 

 

 一到四为对 Student 即 Identity框架的使用,第五节为对 Admin 用户的配置

 

 

一、自定义账号和密码的限制

在 Startup.cs 的 ConfigureServices 方法中可以对 Identity 的账号和密码进行限制:

复制代码
 1             services.AddIdentity<Student, IdentityRole>(opts =>
 2             {
 3 
 4                 opts.User.RequireUniqueEmail = true;
 5                 opts.User.AllowedUserNameCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789";
 6                 opts.Password.RequiredLength = 6;
 7                 opts.Password.RequireNonAlphanumeric = false;
 8                 opts.Password.RequireLowercase = false;
 9                 opts.Password.RequireUppercase = false;
10                 opts.Password.RequireDigit = false;
11             }).AddEntityFrameworkStores<StudentIdentityDbContext>()
12                 .AddDefaultTokenProviders();
复制代码

RequireUniqueEmail 限制每个邮箱只能用于一个账号。

此处 AllowedUserNameCharacters 方法限制用户名能够使用的字符,需要单独输入每个字符。

剩下的设置分别为限制密码必须有符号 / 包含小写字母 / 包含大写字母 / 包含数字。

 

 

 

二、对数据库进行初始化

在此创建一个 StudentInitiator 用以对数据库进行初始化:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task Initial(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             if (userManager.Users.Any())
 7             {
 8                 return;
 9             }
10             IEnumerable<Student> initialStudents = new[]
11             {
12                 new Student()
13                 {
14                     UserName = "U201600001",
15                     Name = "Nanase",
16                     Email = "Nanase@cnblog.com",
17                     PhoneNumber = "12345678910",
18                     Degree = Degrees.CollegeStudent,
19                     MaxBooksNumber = 10,
20                 },
21                 new Student()
22                 {
23                     UserName = "U201600002",
24                     Name = "Ruri",
25                     Email = "NanaseRuri@cnblog.com",
26                     PhoneNumber = "12345678911",
27                     Degree = Degrees.DoctorateDegree,
28                     MaxBooksNumber = 15
29                 },
30             };
31 
32             foreach (var student in initialStudents)
33             {
34                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6,6));
35             }
36         } 
37        }
复制代码

 

为确保能够进行初始化,在 Startup.cs 的 Configure 方法中调用该静态方法:

复制代码
1             app.UseMvc(routes =>
2             {
3                 routes.MapRoute(
4                     name: "default",
5                     template: "{controller=Home}/{action=Index}/{id?}");
6             });
7             DatabaseInitiator.Initial(app.ApplicationServices).Wait();
复制代码

 

Initial 方法中 serviceProvider 参数将在传入 ConfigureServices 方法调用后的 ServiceProvider,此时在 Initial 方法中初始化的数据也会使用 ConfigureServices 中对账号和密码的限制。

此处我们使用账号的后六位作为密码。启动网页后查看数据库的数据:

 

 

 

 

三、建立验证所用的控制器以及视图

 

首先创建一个视图模型用于存储账号的信息,为了方便实现多种登录方式,此处创建一个 LoginType 枚举:

[UIHint] 特性构造函数传入一个字符串用来告知对应属性在使用 Html.EditorFor() 时用什么模板来展示数据。

复制代码
 1     public enum LoginType
 2     {
 3         UserName,
 4         Email,
 5         Phone
 6     }
 7 
 8     public class LoginModel
 9     {
10         [Required(ErrorMessage = "请输入您的学号 / 邮箱 / 手机号码")]
11         [Display(Name = "学号 / 邮箱 / 手机号码")]
12         public string Account { get; set; }
13 
14         [Required(ErrorMessage = "请输入您的密码")]
15         [UIHint("password")]
16         [Display(Name = "密码")]
17         public string Password { get; set; }
18 
19         [Required]
20         public LoginType LoginType { get; set; }
21     }
复制代码

 

   使用支架特性创建一个 StudentAccountController

 

StudentAccount 控制器:

第 5 行判断是否授权以避免多余的授权:

复制代码
 1      public class StudentAccountController : Controller
 2      {
 3         public IActionResult Login(string returnUrl)
 4         {
 5             if (HttpContext.User.Identity.IsAuthenticated)
 6             {
 7                 return RedirectToAction("AccountInfo");
 8             }
 9 
10             LoginModel loginInfo = new LoginModel();
11             ViewBag.returnUrl = returnUrl;
12             return View(loginInfo);
13         }
14     }
复制代码

 

在在 Login 视图中添加多种登录方式,并使视图更加清晰,创建了一个 LoginTypeTagHelper ,TagHelper 可制定自定义 HTML 标记并在最终生成视图时转换成标准的 HTML 标记。

复制代码
 1     [HtmlTargetElement("LoginType")]
 2     public class LoginTypeTagHelper:TagHelper
 3     {
 4         public string[] LoginType { get; set; }
 5 
 6         public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
 7         {
 8             foreach (var loginType in LoginType)
 9             {
10                 switch (loginType)
11                 {
12                     case "UserName": output.Content.AppendHtml($"<option selected=\"selected/\" value=\"{loginType}\">学号</option>");
13                         break;
14                     case "Email": output.Content.AppendHtml(GetOption(loginType, "邮箱"));
15                         break;
16                     case "Phone": output.Content.AppendHtml(GetOption(loginType, "手机号码"));
17                         break;
18                     default: break;
19                 }                
20             }            
21             return Task.CompletedTask;
22         }
23 
24         private static string GetOption(string loginType,string innerText)
25         {
26             return $"<option value=\"{loginType}\">{innerText}</option>";
27         }
28     }
复制代码

 

Login 视图:

25 行中使用了刚建立的 LoginTypeTagHelper:

复制代码
 1 @model LoginModel
 2 
 3 @{
 4     ViewData["Title"] = "Login";
 5 }
 6 
 7 <h2>Login</h2>
 8 <br/>
 9 <div class="text-danger" asp-validation-summary="All"></div>
10 <br/>
11 <form asp-action="Login" method="post">
12     <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/>
13     <div class="form-group">   
14         <label asp-for="Account"></label>
15         <input asp-for="Account" class="form-control" placeholder="请输入你的学号 / 邮箱 / 手机号"/>
16     </div>
17     <div class="form-group">   
18         <label asp-for="Password"></label>
19         <input asp-for="Password" class="form-control" placeholder="请输入你的密码"/>
20     </div>
21     <div class="form-group">
22         <label>登录方式</label>
23         <select asp-for="LoginType">
24             <option disabled value="">登录方式</option>
25             <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType>
26         </select>
27     </div>
28     <input type="submit" class="btn btn-primary"/>
29 </form>
复制代码

 

 

然后创建一个用于对信息进行验证的动作方法。

 

为了获取数据库的数据以及对数据进行验证授权,需要通过 DI(依赖注入) 获取对应的 UserManager 和 SignInManager 对象,在此针对 StudentAccountController 的构造函数进行更新。

StudentAccountController 整体:

复制代码
 1     [Authorize]
 2     public class StudentAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5         private SignInManager<Student> _signInManager;
 6 
 7         public StudentAccountController(UserManager<Student> studentManager, SignInManager<Student> signInManager)
 8         {
 9             _userManager = studentManager;
10             _signInManager = signInManager;
11         }
12 
13         [AllowAnonymous]
14         public IActionResult Login(string returnUrl)
15         {
16             if (HttpContext.User.Identity.IsAuthenticated)
17             {
18                 return RedirectToAction("AccountInfo");
19             }
20 
21             LoginModel loginInfo = new LoginModel();
22             ViewBag.returnUrl = returnUrl;
23             return View(loginInfo);
24         }
25 
26         [HttpPost]
27         [ValidateAntiForgeryToken]
28         [AllowAnonymous]
29         public async Task<IActionResult> Login(LoginModel loginInfo, string returnUrl)
30         {
31             if (ModelState.IsValid)
32             {
33                 Student student =await GetStudentByLoginModel(loginInfo);
34 
35                 if (student == null)
36                 {
37                     return View(loginInfo);
38                 }
39                 SignInResult signInResult = await _signInManager.PasswordSignInAsync(student, loginInfo.Password, false, false);
40 
41                 if (signInResult.Succeeded)
42                 {
43                     return Redirect(returnUrl ?? "/StudentAccount/"+nameof(AccountInfo));
44                 }
45 
46                 ModelState.AddModelError("", "账号或密码错误");
47                             
48             }
49 
50             return View(loginInfo);
51         }
52 
53         public IActionResult AccountInfo()
54         {
55             return View(CurrentAccountData());
56         }
57 
58         Dictionary<string, object> CurrentAccountData()
59         {
60             var userName = HttpContext.User.Identity.Name;
61             var user = _userManager.FindByNameAsync(userName).Result;
62 
63             return new Dictionary<string, object>()
64             {
65                 ["学号"]=userName,
66                 ["姓名"]=user.Name,
67                 ["邮箱"]=user.Email,
68                 ["手机号"]=user.PhoneNumber,
69             };
70         }
71     }
复制代码

_userManager 以及  _signInManager 将通过 DI 获得实例;[ValidateAntiForgeryToken] 特性用于防止 XSRF 攻击;returnUrl 参数用于接收或返回之前正在访问的页面,在此处若 returnUrl 为空则返回 AccountInfo 页面;[Authorize] 特性用于确保只有已授权的用户才能访问对应动作方法;CurrentAccountData 方法用于获取当前用户的信息以在 AccountInfo 视图中呈现。

 

由于未进行授权,在此直接访问 AccountInfo 方法默认会返回 /Account/Login 页面请求验证,可通过在 Startup.cs 的 ConfigureServices 方法进行配置以覆盖这一行为,让页面默认返回 /StudentAccount/Login :

1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.LoginPath = "/StudentAccount/Login";
4             }

 

为了使 [Authorize] 特性能够正常工作,需要在 Configure 方法中使用 Authentication 中间件,如果没有调用 app.UseAuthentication(),则访问带有 [Authorize] 的方法会再度要求进行验证。中间件的顺序很重要:

1             app.UseAuthentication();
2             app.UseHttpsRedirection();
3             app.UseStaticFiles();
4             app.UseCookiePolicy();

 

直接访问 AccountInfo 页面:

 

输入账号密码进行验证:

 

验证之后返回 /StudentAccount/AccountInfo 页面:

 

 

 

四、创建登出网页

简单地调用 SignOutAsync 用以清除当前 Cookie 中的授权信息。

复制代码
 1         public async Task<IActionResult> Logout(string returnUrl)
 2         {
 3             await _signInManager.SignOutAsync();
 4             if (returnUrl == null)
 5             {
 6                 return View("Login");
 7             }
 8 
 9             return Redirect(returnUrl);
10         }
复制代码

 

同时在 AccountInfo 添加登出按钮:

复制代码
 1     @model Dictionary<string, object>
 2     @{
 3         ViewData["Title"] = "AccountInfo";
 4     }
 5     <h2>账户信息</h2>
 6     <ul>
 7         @foreach (var info in Model)
 8         {
 9             <li>@info.Key: @Model[info.Key]</li>
10         }
11     </ul>
12     <br />
13     <a class="btn btn-danger" asp-action="Logout">登出</a>
复制代码

 

 

 

登出后返回 Login 页面,同时 AccountInfo 页面需要重新进行验证。

 

附加使用邮箱以及手机号验证的测试:

 

 

 

 

 

五、基于 Role 的 Identity 授权

修改 StudentInitial 类,添加名为 admin 的学生数组并使用 AddToRoleAsync 为用户添加身份。在添加 Role 之前需要在 RoleManager 对象中使用 Create 方法为 Role 数据库添加特定的 Role 字段:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task InitialStudents(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
 7             if (userManager.Users.Any())
 8             {
 9                 return;
10             }
11 
12             if (await roleManager.FindByNameAsync("Admin")==null)
13             {
14                 await roleManager.CreateAsync(new IdentityRole("Admin"));
15             }
16 
17             if (await roleManager.FindByNameAsync("Student")==null)
18             {
19                 await roleManager.CreateAsync(new IdentityRole("Student"));
20             }
21 
22             IEnumerable<Student> initialStudents = new[]
23             {
24                 new Student()
25                 {
26                     UserName = "U201600001",
27                     Name = "Nanase",
28                     Email = "Nanase@cnblog.com",
29                     PhoneNumber = "12345678910",
30                     Degree = Degrees.CollegeStudent,
31                     MaxBooksNumber = 10,
32                 },
33                 new Student()
34                 {
35                     UserName = "U201600002",
36                     Name = "Ruri",
37                     Email = "NanaseRuri@cnblog.com",
38                     PhoneNumber = "12345678911",
39                     Degree = Degrees.DoctorateDegree,
40                     MaxBooksNumber = 15
41                 }
42             };
43 
44             IEnumerable<Student> initialAdmins = new[]
45             {
46                 new Student()
47                 {
48                     UserName = "A000000000",
49                     Name="Admin0000",
50                     Email = "Admin@cnblog.com",
51                     PhoneNumber = "12345678912",
52                     Degree = Degrees.CollegeStudent,
53                     MaxBooksNumber = 20
54                 },
55                 new Student()
56                 {
57                     UserName = "A000000001",
58                     Name = "Admin0001",
59                     Email = "123456789@qq.com",
60                     PhoneNumber = "12345678910",
61                     Degree = Degrees.CollegeStudent,
62                     MaxBooksNumber = 20
63                 },
64             };
65             foreach (var student in initialStudents)
66             {
67                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6, 6));
68             }
69             foreach (var admin in initialAdmins)
70             {
71                 await userManager.CreateAsync(admin, "zxcZXC!123");
72                 await userManager.AddToRoleAsync(admin, "Admin");
73             }
74         }
75     }
复制代码

 

对 ConfigureServices 作进一步配置,添加 Cookie 的过期时间和不满足 Authorize 条件时返回的 Url:

复制代码
1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.Cookie.HttpOnly = true;
4                 opts.LoginPath = "/StudentAccount/Login";
5                 opts.AccessDeniedPath = "/StudentAccount/Login";
6                 opts.ExpireTimeSpan=TimeSpan.FromMinutes(5);
7             });
复制代码

则当 Role 不为 Admin 时将返回 /StudentAccount/Login 而非默认的 /Account/AccessDeny。

 

 

然后新建一个用以管理学生信息的 AdminAccount 控制器,设置 [Authorize] 特性并指定 Role 属性,使带有特定 Role 的身份才可以访问该控制器。

复制代码
 1     [Authorize(Roles = "Admin")]
 2     public class AdminAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5 
 6         public AdminAccountController(UserManager<Student> userManager)
 7         {
 8             _userManager = userManager;
 9         }
10 
11         public IActionResult Index()
12         {
13             ICollection<Student> students = _userManager.Users.ToList();
14             return View(students);
15         }
16     }
复制代码

 

Index 视图:

复制代码
  1 @using LibraryDemo.Models.DomainModels
  2 @model IEnumerable<LibraryDemo.Models.DomainModels.Student>
  3 @{
  4     ViewData["Title"] = "AccountInfo";
  5     Student stu = new Student();
  6 }
  7 <link rel="stylesheet" href="~/css/BookInfo.css" />
  8 
  9 <script>
 10     function confirmDelete() {
 11         var userNames = document.getElementsByName("userNames");
 12         var message = "确认删除";
 13         var values = [];
 14         for (i in userNames) {
 15             if (userNames[i].checked) {
 16                 message = message + userNames[i].value+",";
 17                 values.push(userNames[i].value);
 18             }
 19         }
 20         message = message + "?";
 21         if (confirm(message)) {
 22             $.ajax({
 23                 url: "@Url.Action("RemoveStudent")",
 24                 contentType: "application/json",
 25                 method: "POST",
 26                 data: JSON.stringify(values),
 27                 success: function(students) {
 28                     updateTable(students);
 29                 }
 30             });
 31         }
 32     }
 33 
 34     function updateTable(data) {
 35         var body = $("#studentList");
 36         body.empty();
 37         for (var i = 0; i < data.length; i++) {
 38             var person = data[i];
 39             body.append(`<tr><td><input type="checkbox" name="userNames" value="${person.userName}" /></td>
 40             <td>${person.userName}</td><td>${person.name}</td><td>${person.degree}</td>
 41             <td>${person.phoneNumber}</td><td>${person.email}</td><td>${person.maxBooksNumber}</td></tr>`);
 42         }
 43     };
 44 
 45     function addStudent() {
 46         var studentList = $("#studentList");
 47         if (!document.getElementById("studentInfo")) {
 48             studentList.append('<tr id="studentInfo">' +
 49                 '<td></td>' +
 50                 '<td><input type="text" name="UserName" id="UserName" /></td>' +
 51                 '<td><input type="text" name="Name" id="Name" /></td>' +
 52                 '<td><input type="text" name="Degree" id="Degree" /></td>' +
 53                 '<td><input type="text" name="PhoneNumber" id="PhoneNumber" /></td>' +
 54                 '<td><input type="text" name="Email" id="Email" /></td>' +
 55                 '<td><input type="text" name="MaxBooksNumber" id="MaxBooksNumber" /></td>' +
 56                 '<td><button type="submit" onclick="return postAddStudent()">添加</button></td>' +
 57                 '</tr>');
 58         }
 59     }
 60     
 61     function postAddStudent() {
 62         $.ajax({
 63             url: "@Url.Action("AddStudent")",
 64             contentType: "application/json",
 65             method: "POST",
 66             data: JSON.stringify({
 67                 UserName: $("#UserName").val(),
 68                 Name: $("#Name").val(),
 69                 Degree:$("#Degree").val(),
 70                 PhoneNumber: $("#PhoneNumber").val(),
 71                 Email: $("#Email").val(),
 72                 MaxBooksNumber: $("#MaxBooksNumber").val()
 73             }),
 74             success: function (student) {
 75                 addStudentToTable(student);
 76             }
 77         });
 78     }
 79 
 80     function addStudentToTable(student) {
 81         var studentList = document.getElementById("studentList");
 82         var studentInfo = document.getElementById("studentInfo");
 83         studentList.removeChild(studentInfo);
 84 
 85         $("#studentList").append(`<tr>` +
 86             `<td><input type="checkbox" name="userNames" value="${student.userName}" /></td>` +
 87             `<td>${student.userName}</td>` +
 88             `<td>${student.name}</td>`+
 89             `<td>${student.degree}</td>` +
 90             `<td>${student.phoneNumber}</td>` +
 91             `<td>${student.email}</td>` +
 92             `<td>${student.maxBooksNumber}</td >` +
 93             `</tr>`);
 94     }
 95 </script>
 96 
 97 <h2>学生信息</h2>
 98 
 99 <div id="buttonGroup">
100     <button class="btn btn-primary" onclick="return addStudent()">添加学生</button>
101     <button class="btn btn-danger" onclick="return confirmDelete()">删除学生</button>
102 </div>
103 
104 
105 <br />
106 <table>
107     <thead>
108         <tr>
109             <th></th>
110             <th>@Html.LabelFor(m => stu.UserName)</th>
111             <th>@Html.LabelFor(m => stu.Name)</th>
112             <th>@Html.LabelFor(m => stu.Degree)</th>
113             <th>@Html.LabelFor(m => stu.PhoneNumber)</th>
114             <th>@Html.LabelFor(m => stu.Email)</th>
115             <th>@Html.LabelFor(m => stu.MaxBooksNumber)</th>
116         </tr>
117     </thead>
118     <tbody id="studentList">
119 
120         @if (!@Model.Any())
121         {
122             <tr><td colspan="6">未有学生信息</td></tr>
123         }
124         else
125         {
126             foreach (var student in Model)
127             {
128                 <tr>
129                     <td><input type="checkbox" name="userNames" value="@student.UserName" /></td>
130                     <td>@student.UserName</td>
131                     <td>@student.Name</td>
132                     <td>@Html.DisplayFor(m => student.Degree)</td>
133                     <td>@student.PhoneNumber</td>
134                     <td>@student.Email</td>
135                     <td>@student.MaxBooksNumber</td>
136                 </tr>
137             }
138         }
139     </tbody>
140 </table>
复制代码

 

使用 Role 不是 Admin 的账户登录:

 

 

使用 Role 为 Admin 的账户登录:

ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 – NanaseRuri – 博客园已关闭评论
2019年2月10日 By mikel 分类: 架构设计

十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别。最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器

来源: 朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh) – lovecindywang – 博客园

十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别。最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器编排的地位确定之后大家又开始实践起以K8S为核心的云原生思想和微服务的结合如何去落地,2018年又多出一个ServiceMesh服务网格的概念,大家又在思考如何引入落地ServiceMesh,ServiceMesh和K8S以及Spring Cloud的关系如何等等。

确实有点乱了,这一波又一波的热潮,几乎每两年都会来一波有关微服务架构理念和平台,许多公司还没完成微服务的改造就又提出了服务+容器道路,又有一些公司打算从微服务直接升级成ServiceMesh。本文尝试总结一下我见过的或实践过的一些微服务落地方式,并且提出一些自己的观点,希望抛砖引玉,大家可以畅谈一下自己公司的微服务落地方式。

1、微服务v0.1——古典玩法


(图中灰色部分代表元数据存储区域,也就是Service和Endpoint关系所保存的地方,之后所有的图都是这样)

其实在2006年在使用.NET Remoting做服务拆分的时候(其实当时我们没有意识到这叫服务拆分,这是打算把一些逻辑使用独立的进程来承载,以Windows服务形式安装在不同服务器上分散压力),我们使用了F5来做服务的负载均衡。没有所谓的服务发现,针对每一个服务,我们直接在程序配置文件中写死F5的IP地址和端口,使用Excel来记录所有服务在F5的端口地址以及服务实际部署的IP:端口,然后在F5进行配置。F5在这里做了负载均衡、简单的路由策略(相同的客户端总是优先路由到相同的后端)以及简单的白名单策略等等。

2、微服务v0.2——改进版古典玩法


之后尝试过这种改进版的古典玩法。相比v0.1的区别是,不再使用硬件F5了,而是使用几组软件反向代理服务器,比如Nginx来做服务负载均衡(如果是TCP的负载均衡的话可以选择HaProxy),Nginx的配置会比F5更方便而且还不花钱。由于生产环境Nginx可能是多组,客户端不在配置文件中写死Nginx地址而是把地址放到了配置中心去,而Nginx的配置由源码仓库统一管理,运维通过文件同步方式或其它方式从源码仓库拉取配置文件下发到不同的Nginx集群做后端服务的配置(Nginx的配置也不一定需要是一个大文件放所有的配置,可以每一组服务做一个配置文件更清晰)。

虽然我的标题说这是古典玩法,但是可以说很多公司如果没有上RPC,没有上Spring Cloud,也没有上K8S的话很可能就是这样的玩法。无论是v0.2还是v0.1,本质上服务是固定在虚拟机或实体机部署的,如果需要扩容,需要迁移,那么肯定需要修改反向代理或负载均衡器的配置。少数情况下,如果调整了反向代理或负载均衡器的IP地址,那么还可能会需要修改客户端的配置。

3、微服务v0.5——SOA ESB玩法


SOA的一个特点是使用了服务总线,服务总线承担了服务的发现、路由、协议转换、安全控制、限流等等。2012年我参与了一个大型MMO游戏《激战2》项目的技术整合工作,这个游戏整个服务端就是这种架构。它有一个叫做Portal的服务总线,所有游戏的十几个子服务都会把自己注册到服务总线,不管是什么服务需要调用什么接口,都是在调用服务总线,由服务总线来进行服务的寻址路由和协议转换,服务总线也会做服务的精细化限流,每一个用户都有自己的服务请求队列。这种架构的好处是简单,服务总线承担了所有工作,但是服务总线的压力很大,承担了所有的服务转发工作。同时需要考虑服务总线本身如何进行扩容,如果服务总线是有状态的,显然要进行扩容不是这么简单。对于游戏服务器来说,扩容可能不是一个强需求,因为游戏服务天然会按照大区进行分流,一个大区的最大人数上限是固定的。

貌似互联网公司这样玩的不多,传统企业或是游戏服务端是比较适合服务总线这种架构的,如果服务和服务之间的协议不统一的话,要在客户端做协议转换的工作比较痛苦,如果可以由统一的中间层接入所有协议统一进行转换的话,客户端会比较轻量,但是这种架构的很大问题在于服务总线的扩容和可靠性。

4、微服务v1.0——传统服务框架玩法


上图是大多数RPC框架的架构图。大多数早期的微服务实践都是RPC的方式,最近几年Spring Cloud盛行后其实Spring Cloud的玩法也差不多,只是Spring Cloud推崇的是JSON over HTTP的RESTful接口,而大多数RPC框架是二进制序列化over TCP的玩法(也有JSON over HTTP的RPC)。

其实RPC框架我个人喜欢JSON over HTTP,虽然我们知道HTTP和JSON序列化性能肯定不如一些精简的二进制序列化+TCP,但是优点是良好的可读性、测试方便、客户端开发方便,而且我不认为15000的QPS和20000的QPS对于一般应用有什么区别。

总的来说,我们会有一个集群化的分布式配置中心来充当服务注册的存储,比如ZK、Consul、Eureka或etcd。我们的服务框架会有客户端和服务端部分,客户端部分会提供服务的发现、软负载、路由、安全、策略控制等功能(可能也会通过插件形式包含Metrics、Logging、Tracing、Resilience等功能),服务端部分对于RPC框架会做服务的调用也会辅助做一些安全、策略控制,对于RESTful的话就服务端一般除了监控没有额外的功能。

比如使用Spring Cloud来玩,那么:

  • Service Discovery:Eureka、Open Feign
  • Load Balancing:Ribbon、Spring Cloud LoadBalancer
  • Metrics:Micrometer、Spring Boot Actuator
  • Resilience:Hystrix、Resilience4j
  • Tracing:Sleuth、Zipkin

在之前《朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子)》一文中,我有一个完整的例子介绍过Spring Cloud的这套玩法,可以说的确Spring Cloud给了我们构建一套微服务体系最基本的东西,我们只需要进行一些简单的扩展和补充,比如灰度功能,比如更好的配置服务,就完全可以用于生产。
这种模式和之前0.x的很大区别是,服务的注册有一个独立的组件,注册中心完成,通过配合客户端类库的服务发现,至少服务的扩容很轻松,扩容后也不需要手动维护负载均衡器的配置,相当于服务端从死到活的一个重大转变。而且在1.0的时代,我们更多看到了服务治理的部分,开始意识到成百上千的服务,如果没有Metrics、Logging、Tracing、Resilience等功能来辅助的话,微服务就是一个灾难。

Spring Cloud已经出了G版了,表示Netflix那套已经进入了维护模式,许多程序员表示表示扶我起来还能学。我认为Spring Cloud这个方向其实是挺对的,先有开源的东西来填补空白,慢慢再用自己的东西来替换,但是开发比较苦,特别是一些公司基于Spring Cloud辛苦二次开发的框架围绕了Netflix那套东西来做的会比较痛苦。总的来说,虽然Spring Cloud给人的感觉很乱,变化很大,大到E到G版的升级不亚于在换框架,而且组件质量层次不齐,但是它确实是一无所有的创业公司能够起步微服务的不多的选择之一。如果没有现成的框架(不是说RPC框架,RPC框架虽是微服务功能的80%重点,但却是代码量20%的部分,工作量最大的是治理和整合那套),基于Spring Cloud起步微服务,至少你可以当天起步,1个月完成适合自己公司的二次开发改造。

5、微服务v2.0——容器+K8S容器调度玩法


K8S或者说容器调度平台的引入是比较革命性的,容器使得我们的微服务对环境的依赖可以打包整合进行随意分发,这是微服务节点可以任意调度的基础,调度平台通过服务的分类和抽象,使得微服务本身的部署和维护实现自动化,以及实现更上一层楼的自动伸缩。在1.x时代,服务可以进行扩缩容,但是一切都需要人工介入,在2.x时代,服务本身在哪里存在甚至有多少实例存在并不重要,重要的只是我们有多少资源,希望服务的SLA是怎么样的,其余留给调度平台来调度。

如果说1.0时代大家纠结过Dubbo还是Spring Cloud,2.0时代我相信也有一些公司上过Mesos的“贼船”,我们不是先知很难预测什么框架什么技术会在最后存活下来,但是这却是也给技术带来了不少痛苦,相信还是有不少公司在干Mesos转K8S的事情。

如果引入了K8S,那么服务发现可以由K8S来做,不一定需要Eureka。我们可以为Pod创建Service,通过Cluster虚拟IP的方式(如上图所示,通过IP tables)路由到Pod IP来做服务的路由(除了Cluster IP方式也有的人对于内部连接会采用Ingress方式去做,路由方面会更强大,不过这是不是又类似v0.2了呢?)。当然,我们还可以更进一步引入内部DNS,使用内部域名解析成Cluster IP,客户端在调用服务的时候直接使用域名(域名可以通过配置服务来配置,也可以直接读取环境变量)即可。如果这么干的话其实就没有Eureka啥事了,有的公司没有选择这种纯K8S服务路由的方式还是使用了注册中心,如果这样的话其实服务注册到注册中心的就是Pod IP,还是由微服务客户端做服务发现的工作。我更喜欢这种方式,我觉得K8S的服务发现还是弱了一点,而且IP tables的方式让人没有安全感(IPVS应该是更好的选择),与其说是服务发现,我更愿意让K8S只做容器调度的工作以及Pod发现的工作。

虽然K8S可以做一部分服务发现的工作,我们还是需要在客户端中去实现更多的一些弹力方面的功能,因此我认为2.0时代只是说是微服务框架结合容器、容器调度,而不能是脱离微服务框架本身完全依靠K8S实现微服务。2.0和1.0的本质区别或者说增强还是很明显,那就是我们可以全局来统筹解决我们的微服务部署和可靠性问题,在没有容器和容器调度这层抽象之前,有的公司通过实现自动化虚拟机分配拉起,加上自动化初始脚本来实现自动的微服务调度扩容,有类似的意思,但是非常花时间而且速度慢。K8S真正让OPS成为了DEV而不是执行者,让OPS站在总体架构的层面通过DEV(咱不能说开发DSL文件不算开发吧)资源和资源之间的关系来统筹整个集群。在只有十几个微服务若干台服务器的小公司可能无法发挥2.0容器云的威力,但是服务器和服务一多,纯手工的命令式配置容易出错且难以管理,K8S真的释放了几十个运维人力。

6、微服务v3.0——ServiceMesh服务网格玩法


在之前提到过几个问题:

  • SOA的模式虽然简单,但是集中的Proxy在高并发下性能和扩容会是问题
  • 传统的RPC方式,客户端很重,做了很多工作,甚至协议转换都在客户端做,而且如果涉及到跨语言,那么RPC框架需要好几套客户端和服务端
  • K8S虽然是一个重要的变革,但是在服务调度方面还是太弱了,它的专项在于资源调度

于是ServiceMesh服务网格的概念腾空而出,巧妙解决了这几个问题:

  • 采用边车模式的Proxy随服务本身部署,一服务一边车与服务共生死(当然,有的公司会使用类似ServiceBus的Global Proxy作为Sidecar Proxy的后备,防止服务活着Sidecar死了的情况)可以解决性能问题
  • Sidecar里面做了路由、弹力等工作,客户端里可以啥都不干,如上图所示,上图是Istio的架构,Istio的理念是把ServiceMesh分成了数据面和控制面,数据面主要是负责数据传输,由智能代理负责(典型的组件是Envoy),控制面由三大组件构成,Pilot负责流量管理和配置(路由策略、授权策略)下发,Mixer负责策略和数据上报(遥测),Citadel用于密钥和证书管理
  • 由于我们双边都走Sidecar Proxy,我们对于流量的进出都可以做很细粒度的控制,这个控制力度是之前任何一种模式都无法比拟的,这种架构的方式就像把服务放到了网格之中,服务连接外部的通讯都由网格进行,服务本身轻量且只需要关注业务逻辑,网格功能强大而灵活
  • 对于Proxy的流量劫持可以使用IP table进行拦截,对于服务本身无感知,而且Sidecar可以自动注入Pod,和K8S进行自动整合,无需特殊配置,做到透明部署透明使用
  • Pilot是平台无关的,采用适配器形式可以和多个平台做整合,如果和K8S整合的话,它会和API Server进行通讯,订阅服务、端点的信息,然后把信息转变成Istio自己的格式作为路由的元数据
  • Mixer期望的是抽象底层的基础设施,不管是Logging还是Metrics、Tracing,在之前RPC时代的做法是客户端和服务端都会直接上报信息到InfluxDb、Tracing Server等,这让客户端变得很臃肿,Istio的理念是这部分对接后端的工作应该由统一的组件进行,不但使得Proxy可以更轻而且可以通过Plugin机制对接各种后端基础设施

说了这么多ServiceMesh的优势,我们来看一下这种模式的性能问题。想一下各种模式下客户端要请求服务端整个HTTP请求(跳)次数:

  • 古典模式:2跳,代理转发一次
  • SOA模式:2跳,总线转发一次
  • 传统模式:1跳,客户端直连服务端
  • K8S Service模式:1跳(路由表会有一定损耗)
  • ServiceMesh模式:3跳(其中2跳是localhost回环)

总的来说,3跳并不是ServiceMesh的瓶颈所在,而更多的可能性是Istio的倔强的架构理念。Istio认为策略和遥测不应该耦合在Sidecar Proxy应该放到Mixer,那么相当于在调用服务的时候还需要额外增加Mixer的同步请求(来获得策略方面的放行)。Istio也在一直优化这方面,比如为Mixer的策略在Proxy做本地缓存,为遥测数据做批量上报等等。虽然经过层层优化,但是Istio目前的TPS不足2000,还是和一般的RPC能达到的20000+有着十倍的差距,说不定将来Istio会有架构上的妥协,把Mixer变为非直接依赖,策略方面还是采用类似Pilot统一管理配置下发的方式,遥测方面还是由Sidecar直接上报数据到Mixer。

我个人认为,ServiceMesh是一个非常正确的道路,而且ServiceMesh和K8S结合会更好,理由在于:

  • K8S让资源调度变得自由,但微服务调度不是其所长也不应该由它深入实现
  • 以Istio为代表的ServiceMesh做了K8S少的,但是微服务又必须的那块工作
  • Istio的设计方面和K8S极其相似,低耦合,抽象的很好,两者结合的也很好,我非常喜欢和赞同Agent+统一的资源管理配置下发的方式(K8S的Agent就是KubeProxy和Kubelet,Istio的Agent就是Sidecar Proxy),这是松耦合和高性能的平衡
  • 在复杂的异构环境下,多协议的内部通讯,跨平台跨语言的内部通讯很常见,如果采用传统方式,框架太胖太重,把这部分工作从内部剥离出来好处多多

但是,可以看到目前ServiceMesh还不算非常成熟,Istio在不断优化中,Linkerd 2.x也想再和Istio拼一下,到底谁会胜出还难以知晓,经过之前Dubbo vs Spring Cloud的折腾,Mesos vs K8S的折腾,VM vs Docker的折腾,是否还能经得起折腾Istio vs Linkerd 2呢?我建议还是再看一看,再等一等。

7、畅想Everything Mesh模式?


之前看到过ShardingSphere受到ServiceMesh的理念影响提出了DB Mesh的架构。其实DB Proxy的中间件已经存在很多年了(集中化的Proxy类似服务总线的方式),DB Mesh把Proxy也变为轻量的Sidecar方式,DB的访问也都走本地代理。那么这里我也在想,是不是有可能所有东西都有本地的代理呢?

作为应用服务本身而言,只需要和本地代理做通讯调用外部服务、缓存、数据库、消息队列,不需要关心服务和资源所在何地,以及背后的实际服务的组件形态。当然,这只是一个畅想了,对于有状态的资源,Mesh的难度很大,对于类似DB这样的资源因为调用层次并不复杂,也不太会存在异构场景,Mesh的意义不大,综合起来看Everything Mesh的投入产出比相比Service Mesh还是小很多。

8、Spring Cloud、K8S和ServiceMesh的关系

如果搞Java微服务的话,Spring Boot是离不开的,但是是否要用Spring Cloud呢?我的观点是,在目前阶段如果没有什么更好的选择,还是应该先用。Spring Cloud和K8S首先并不是矛盾的东西,K8S是偏运维的,主要做资源整合和管理,如果彻底没有服务治理框架纯靠K8S的话会很累,而且功能不完整。开发和架构可以在Spring Cloud方面深耕,运维可以在容器和K8S方面发力,两套体系可以协作形成目前来说比较好的微服务基石。至于K8S的推行,这一定是一个正确的方向,而且和软件架构方面的改进工作一点不矛盾,毕竟K8S是脱离于具体语言和平台的。

至于Service Mesh,它做的事情和Spring Cloud是有很多重复的,在将来Istio如果发展的更好的情况下,应该可以替代Spring Cloud,开发人员只需要用Spring Boot开发微服务即可,客户端方面也可以很瘦,不需要过多关心服务如何通讯和路由,服务的安全、通讯、治理、控制都由Service Mesh进行(但是,是否有了Sidecar,客户端真的完全不需要SDK了呢?我认为可能还是需要的,对于Tracing,如果没有客户端部分显然是不完整的,虽然Sidecar是localhost但是还是跨进程了)。

Spring Cloud目前虽然针对K8S和Istio做了一些整合,但是并没看到一套针对ServiceMesh的最佳实践出来,是否将来Spring Cloud会在微服务这方面做退化给ServiceMesh让步还不得而知。总的来说,长期我看好Spring Boot + K8S + Istio的组合,短期我认为还是Spring Boot + K8S + Spring Cloud这么用着。

9、总结

本文总结了各种微服务落地的形态,由于技术多样,各种理念层出不穷,造成了微服务的落地方式真的很难找到两家相同的公司,本文中我们介绍了:

  • 客户端写死地址+F5代理的方式
  • 客户端把地址配置在配置服务+Nginx代理的方式
  • SOA+集中式ESB的方式
  • 传统的具有注册中心的服务框架SDK形式
  • 服务框架+K8S方式
  • K8S Service Iptables路由方式
  • ServiceMesh代理3跳转发方式

当然,可能还会有更多的方式:

  • 内部DNS方式(直接DNS轮询)
  • K8S内部服务走Ingress方式(内部服务也走Ingress,类似所有服务Nginx代理的方式)
  • ServiceMesh代理2跳转发方式(可以根据需要跳过远端的Sidecar来提高性能等等)
  • 瘦服务框架SDK+ServiceMesh方式(也就是还是有一个小的SDK来对接ServiceMesh的Sidecar,而不是让应用程序自己发挥Http Client,这个方式的好处在于更灵活,这个SDK可以在这一层再做一次路由,甚至在Sidecar出问题的时候直接把流量切换出去,切换为直连远端或统一的Global Proxy)

也可能很多公司在混用各种方式,具有N套服务注册中心,正在做容器化迁移,想想就头痛,微服务的理念层出不穷伴随着巨头之间的技术战役,苦的还是架构和开发,当然,运维可能也苦,2019新年快乐,Enjoy微服务!

朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh) – lovecindywang – 博客园已关闭评论
2019年2月10日 By mikel 分类: 数据库

来源: 怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢 – qsa – 博客园

怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢?本文中给出了一些方法。

SQL查询今天的记录:

  1. datediff(day,[Datetime],getdate())=0 把Datetime换为你的相应字段;

SQL查询昨天的记录:

  1. datediff(day,[Datetime],getdate())=1 把Datetime换为你的相应字段,getdate()-Datetime即为时间差。

本月记录:

  1. SELECT * FROM 表 WHERE datediff(month,[dateadd],getdate())=0

本周记录:

  1. SELECT * FROM 表 WHERE datediff(week,[dateadd],getdate())=0

本日记录:

  1. SELECT * FROM 表 WHERE datediff(day,[dateadd],getdate())=0

一天

  1. select * from T_news where datediff(day,addtime,getdate())=0

三天

  1. select * from T_news where datediff(day,addtime,getdate())<= 2 and datediff(day,addtime,getdate())>= 0

一周

  1. select * from T_news WHERE (DATEPART(wk, addtime) = DATEPART(wk, GETDATE())) AND (DATEPART(yy, addtime) = DATEPART(yy, GETDATE()))

注意:此时不能用 datediff 差值为7,因为,datediff只表示间隔数

一月

  1. select * from T_news WHERE (DATEPART(yy, addtime) = DATEPART(yy, GETDATE())) AND (DATEPART(mm, addtime) = DATEPART(mm, GETDATE()))

一季度

  1. select * from T_news where DATEPART(qq, addtime) = DATEPART(qq, GETDATE()) and DATEPART(yy, addtime) = DATEPART(yy, GETDATE())
怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢 – qsa – 博客园已关闭评论
2019年1月22日 By mikel 分类: 数据库

来源: Sql中CHARINDEX用法 – 齐_大圣 – 博客园

CHARINDEX作用

写SQL语句我们经常需要判断一个字符串中是否包含另一个字符串,但是SQL SERVER中并没有像C#提供了Contains函数,不过SQL SERVER中提供了一个叫CHAEINDX的函数,顾名思义就是找到字符(char)的位置(index),既然能够知道所在的位置,当然就可以判断是否包含在其中了。

通过CHARINDEX如果能够找到对应的字符串,则返回该字符串位置,否则返回0。

基本语法如下:

CHARINDEX ( expressionToFind , expressionToSearch [ , start_location ] )

expressionToFind :目标字符串,就是想要找到的字符串,最大长度为8000 。

expressionToSearch :用于被查找的字符串。

start_location:开始查找的位置,为空时默认从第一位开始查找。

 

CHAEINDEX示例

1.简单用法

select charindex(‘test’,’this Test is Test’)

查询结果:  

2.增加开始位置

select charindex(‘test’,’this Test is Test’,7)

查询结果:

3.大小写敏感

select charindex(‘test’,’this Test is Test’COLLATE Latin1_General_CS_AS)

查询结果:

返回结果为0???,不要怀疑你的眼睛,因为大小写敏感,找不到test所以返回的就是0,默认情况下, SQL SERVER是大小不敏感的,所以我们简单示例中返回结果不为0,但是有些时候我们需要特意去区分大小写,因此专门SQL SERVE提供了特殊的关键字用于查询时区分大小写,其中CS为Case-Sensitve的缩写。

4.大小写不敏感

select charindex(‘Test’,’this Test is Test’COLLATE Latin1_General_CI_AS)

查询结果:

我们也可以这样说明是大小写不敏感,其中CI是Case-InSensitve的缩写,即大小写不敏感,当然我们没必要多此一举。

 

PATINDEX

和CHARINDEX类似,PATINDEX也可以用来判断一个字符串中是否包含另一个字符串,两种的差异在于,前者是全匹配,后者支持模糊匹配。

1.简单示例

select PATINDEX(‘%ter%’,’interesting data’)

查询结果:

2.简单示例2

select PATINDEX(‘%t_ng%’,’interesting data’)

查询结果:

 

PATINDEX也允许支持大小写敏感,做法和CHARINDEX一样,此处不再累述。

参考:

https://docs.microsoft.com/en-us/sql/t-sql/functions/charindex-transact-sql

https://docs.microsoft.com/en-us/sql/t-sql/functions/patindex-transact-sql

Sql中CHARINDEX用法 – 齐_大圣 – 博客园已关闭评论
2019年1月16日 By mikel 分类: JavaScript

来源: ECharts.js学习(三)交互组件 – leoxuan – 博客园

ECharts.js有很多的交互组件,一般经常用到的组件有这些:

title:标题组件,包含主标题和副标题。

legend:图例组件,展现了不同系列的标记(symbol),颜色和名字。可以通过点击图例控制哪些系列不显示。

xAxis:直角坐标系 grid 中的 x 轴,一般情况下单个 grid 组件最多只能放左右两个 x 轴,多于两个 x 轴需要通过配置 offset 属性防止同个位置多个 x 轴的重叠。

yAxis:直角坐标系 grid 中的 y 轴,一般情况下单个 grid 组件最多只能放左右两个 y 轴,多于两个 y 轴需要通过配置 offset 属性防止同个位置多个 Y 轴的重叠。

tooltip:提示框组件,就是当你的鼠标悬浮在图表上的提示内容。

toolbox:工具栏组件。内置有导出图片、数据视图、动态类型切换、数据区域缩放、重置五个工具。

series:系列列表。我理解为数据列表。这里可以定义每组数据内容,以及数据的展现形式。

timeline:提供了在多个ECharts option 之间进行切换、播放等操作的功能。

dataZoom:用于区域缩放,从而能自由关注细节的数据信息,或者概览数据整体,或者去除离群点的影响。

….

官方给出的案例是dataZoom组件。它是用于区域缩放,从而能自由关注细节的数据信息,或者概览数据整体,或者去除离群点的影响。主要是对 数轴(axis) 进行操作。

效果展示

 

toolbox组件

其中很多组件其实我们都会用到,不过使用的都是一些基本配置。比如title组件,往往只写一个text 值。legend,会一些每个系列数据的name等等。

因为后面项目需要将图表保存为图片,以及一种数据多种展现形势,所以就研究一下toolbox组件的使用。

toolbox参数:

show:工具栏默认是隐藏的。所以一定要设置show为true显示出来。

orient:工具栏的的布局方向,可选值有horizontal(横向)和vertical(竖向)。默认值是horizontal

itemSize:工具栏的大小。默认值是15。

itemGap:工具栏每个工具之间的距离,默认值是10。

showTitle:鼠标悬浮的是否显示每个工具的说明,默认是true。

feature:这个是设置工具栏里要显示哪些工具,以及这些工具的样式等。

默认的插件工具:

savaAsImage:保存图片

restore:还原配置

dataView:数据视图工具,可以展现图表所用的数据,并且可以编辑数据,再将编辑后的数据展示出来。同时也可以设置为数据为只读。

optionToContent:并且可以通过对显示出来的数据进行排版编辑,以HTML展现。

optionToOption:在使用 optionToContent 的情况下,如果支持数据编辑后的刷新,需要自行通过该函数实现组装 option 的逻辑。

dataZoom:数据区域缩放。目前只支持直角坐标系的缩放。

xAxisIndex、yAxisIndex:分别控制xAxis和yAxis轴的缩放。

除了使用默认的工具意外,我们还可以根据需求自定义工具。需要注意的是,每个自定义的工具,名称必须以“my”打头。在onclick函数中编写需要进行的操作。

 

复制代码
toolbox:{
    show:true,
    orient:'vertical',                
    feature:{
        magicType:{type:['line','bar']},
        restore:{},
        saveAsImage:{},
        dataZoom:{
            show:true,
            xAxisIndex:[0,3]
        },
        myTool1:{
            show:true,
            title:'自定义工具一',
            icon: 'path://M432.45,595.444c0,2.177-4.661,6.82-11.305,6.82c-6.475,0-11.306-4.567-11.306-6.82s4.852-6.812,11.306-6.812C427.841,588.632,432.452,593.191,432.45,595.444L432.45,595.444z M421.155,589.876c-3.009,0-5.448,2.495-5.448,5.572s2.439,5.572,5.448,5.572c3.01,0,5.449-2.495,5.449-5.572C426.604,592.371,424.165,589.876,421.155,589.876L421.155,589.876z M421.146,591.891c-1.916,0-3.47,1.589-3.47,3.549c0,1.959,1.554,3.548,3.47,3.548s3.469-1.589,3.469-3.548C424.614,593.479,423.062,591.891,421.146,591.891L421.146,591.891zM421.146,591.891',
            
            onclick:function(){
                alert("this is myTool1");
            }
        },
        myTool2:{
            show:true,
            title:'自定义工具二',
            icon: 'image://http://echarts.baidu.com/images/favicon.png',                        
            onclick:function(){
                alert("this is myTool2");
            }
        }
    }
}
复制代码

 

magicType:设置可切换的图表类型。目前支持的只有4种,line折线图、bar柱状图、stack堆叠模式、tiled平铺模式。

brush:选框组件的控制按钮。

iconStyle:公用的icon样式设置

zlevel:所有图形的zlevel值。zlevel用于Canvas分层。

z:所有图形的z值。z不会创建Canvas层。比zlevel等级低。

left、top、right、bottom、width、height:工具栏的样式,边距设置。

 

 

复制代码
<script type="text/javascript">        
    //初始化echarts实例
    var myChart = echarts.init(document.getElementById('chartmain'));    

    var option = {
        title:{
            text:"马云和马化腾期末成绩图",
            subtext:'本图表纯属虚构',                
        },
        anmation:false,
        legend:{
            data:["马云成绩","马化腾成绩"],
            left:'50%',
            top:5
        },
        tooltip:{
            trigger:"axis"
        },
        xAxis:{
            type:'category',
            boundaryGap:false,
            data:['语文','数学','英语','历史','体育','生物','化学']
        },
        yAxis:{
            type:'value',
            axisLabel:{
                formatter:'{value}分'
            },
            min:20
        },
        toolbox:{
            show:true,
            orient:'vertical',
            itemSize:20,
            itemGap:20,

            feature:{
                dataView:{
                    readOnly:true,
                    backgroundColor:'#f5f5f5',
                    optionToContent:function(opt){
                        var axisData = opt.xAxis[0].data;
                        var series = opt.series;
                        var table ='<table style="width:100%;text-align:center;border:1px solid red;"><tbody><tr>'
                                    +'<td>学生</td>'
                                    +'<td>'+series[0].name+'</td>'
                                    +'<td>'+series[1].name+'</td>'
                                    +'</tr>';
                        for (var i = 0; i < axisData.length; i++) {
                            table +='<tr>'
                                    +'<td>'+axisData[i]+'</td>'
                                    +'<td>'+series[0].data[i]+'</td>'
                                    +'<td>'+series[1].data[i]+'</td>'
                                    +'</tr>'
                        }
                        table +='</tbody></table>';
                        return table;
                    }                        
                },
                dataZoom:{
                    show:true,
                    xAxisIndex:[0,3]
                },
                magicType:{type:['line','bar','stack','tiled']},
                restore:{},
                saveAsImage:{},                    
                myTool1:{
                    show:true,
                    title:'自定义工具一',
                    icon: 'path://M432.45,595.444c0,2.177-4.661,6.82-11.305,6.82c-6.475,0-11.306-4.567-11.306-6.82s4.852-6.812,11.306-6.812C427.841,588.632,432.452,593.191,432.45,595.444L432.45,595.444z M421.155,589.876c-3.009,0-5.448,2.495-5.448,5.572s2.439,5.572,5.448,5.572c3.01,0,5.449-2.495,5.449-5.572C426.604,592.371,424.165,589.876,421.155,589.876L421.155,589.876z M421.146,591.891c-1.916,0-3.47,1.589-3.47,3.549c0,1.959,1.554,3.548,3.47,3.548s3.469-1.589,3.469-3.548C424.614,593.479,423.062,591.891,421.146,591.891L421.146,591.891zM421.146,591.891',
                    
                    onclick:function(){
                        alert("this is myTool1");
                    }
                },
                myTool2:{
                    show:true,
                    title:'自定义工具二',
                    icon: 'image://http://echarts.baidu.com/images/favicon.png',                        
                    onclick:function(){
                        alert("this is myTool2");
                    }
                }
                
            },            

        },
        series:[
            {
                name:'马云成绩',
                type:'line',
                data:[90,88,75,82,95,89,97],
                markLine:{
                    data:[{type:'average',name:'平均值'}]
                },
                markPoint:{
                    data:[
                        {type:'max',name:'最高分'},
                        {type:'min',name:'最低分'}
                    ]
                }                    
            },
            {
                name:'马化腾成绩',
                type:'line',
                data:[55,45,99,60,35,45,74],
                markLine:{
                    data:[{type:'average',name:'平均值'}]
                },
                markPoint:{
                    data:[
                        {type:'max',name:'最高分'},
                        {type:'min',name:'最低分'}
                    ]
                }
            }
        ]
    }

    myChart.setOption(option);
</script>
复制代码

 

 

效果展示

ECharts.js学习(三)交互组件 – leoxuan – 博客园已关闭评论
2019年1月16日 By mikel 分类: JavaScript

Echarts 数据绑定 简单的统计表已经可以生成,不过之前图标数据都是直接写在参数里面的,而实际使用中,我们的数据一般都是异步读取的。EChart.js对于数据异步读取这块提供了异步加载的

来源: ECharts.js学习(二)动态数据绑定 – leoxuan – 博客园

charts 数据绑定

简单的统计表已经可以生成,不过之前图标数据都是直接写在参数里面的,而实际使用中,我们的数据一般都是异步读取的。EChart.js对于数据异步读取这块提供了异步加载的方法。

绑定多组数据

很多时候需要展示的数据不单单是一组数据,很多时候会进行一个数据对比。这个时候只需要在series中增加一组数据,legend中添加一下这个数据组的name

复制代码
<!DOCTYPE html>
<html>
<head>
    <title>ECharts.js 数据绑定</title>
    <meta charset="utf-8">
    <script type="text/javascript" src="js/echarts.js"></script>
</head>
<body>
    <div id="chartmain" style="width:600px; height: 400px;"></div>
    <script type="text/javascript">
        //指定图标的配置和数据
        var option = {
            title:{
                text:'ECharts 数据统计'
            },
            legend:{
                data:['访问量','用户量']
            },
            xAxis:{
                data:["Android","IOS","PC","Other"]
            },
            yAxis:{},
            series:[
            {
                name:'访问量',
                type:'bar',
                data:[180,420,333,83]
            },
            {
                name:'用户量',
                type:'bar',
                data:[125,330,230,60]
            }
            ]
        };
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));

        //使用制定的配置项和数据显示图表
        myChart.setOption(option);
    </script>
</body>
</html>
复制代码

效果展示

数据异步加载

EChart中实现异步数据的更新非常简单,在图表初始化后不管任何时候只要通过 jQuery 等工具异步获取数据后通过 setOption 填入数据和配置项就行。

绑定数据的方式有两种,一种是写写好一些图表参数,然后数据留空,然后在异步读取数据的时候,绑定数据。还有一种就是直接异步读取数据的时候同时设置图表参数和数据绑定。

首先我们准备一份需要加载的数据文件data.json,数据内容:

{"name":["Android","IOS","PC","Other"],"data":[420,200,360,100]}

第一种异步加载的时候设置图表参数和绑定数据

复制代码
    <script type="text/javascript">        
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));        
        //异步加载的配置项和数据显示图表
        $.get('data.json').done(function (data) {
            data = eval('('+data+')');            
            myChart.setOption({
                title:{
                    text:'ECharts 异步加载数据'
                },
                tooltip:{},
                legend:{
                    data:['访问量']
                },
                xAxis:{
                    data:data.name
                },
                yAxis:{},
                series:[
                    {
                        name:'访问量',
                        type:'bar',
                        data:data.data
                    }
                ]
            })
        })


    </script>
复制代码

第二种先设置图表参数,后绑定数据

复制代码
<script type="text/javascript">        
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));        
        //设置图标配置项
        myChart.setOption({
            title:{
                text:'ECharts 异步加载数据'
            },
            tooltip:{},
            legend:{
                data:['访问量']
            },
            xAxis:{
                data:[]
            },
            yAxis:{},
            series:[
                {
                    name:'访问量',
                    type:'bar',
                    data:[]
                }
            ]
        })
        //异步加载数据
        $.get('data.json').done(function (data) {
            data = eval('('+data+')');            
            myChart.setOption({                
                xAxis:{
                    data:data.name
                },                
                series:[
                    {    
                        //根据名字对应到相应的系列
                        name:"访问量",
                        data:data.data
                    }
                ]
            })
        })
    </script>
复制代码

效果展示

因为是异步加载,所以有时候数据加载会慢,或者延迟。在数据没有加载前,图表这样的。面对这样的图表,肯定会觉得这是没有数据吗,还是图表有问题.对于这块ECharts增加了一个加载动画。

Loading动画加载

复制代码
        //打开loading动画
        myChart.showLoading();
        //加载数据函数
        function bindData(){
            //为了效果明显,我们做了延迟读取数据
            setTimeout(function(){                
                //异步加载数据
                $.get('data.json').done(function (data) {                    
                    //获取数据后,隐藏loading动画
                    myChart.hideLoading();
                    data = eval('('+data+')');            
                    myChart.setOption({
                        xAxis:{
                            data:data.name
                        },
                        series:[
                            {
                                //根据名字对应到相应的系列
                                name:"访问量",
                                data:data.data
                            }
                        ]
                    })
                })
            },2000)
        }
        
        bindData();
复制代码

效果展示

数据动态实时更新

复制代码
<script type="text/javascript">        
    //初始化echarts实例
    var myChart = echarts.init(document.getElementById('chartmain'));
    var base = + new Date(2017,3,8);
    var oneDay = 24*3600*1000;
    var date = [];
    var data = [Math.random()*150];
    var now = new Date(base);
    var day = 30;
    function addData(shift){
        now = [now.getFullYear(),now.getMonth()+1,now.getDate()].join('/');        
        date.push(now);        
        data.push((Math.random()-0.5)*10+data[data.length-1]);
        if (shift) {
            console.log(data);
            date.shift();
            data.shift();
        }
        now = new Date(+new Date(now)+oneDay);        
    }

    for (var i = 0; i < day; i++) {
        addData();
    }
    //设置图标配置项
    myChart.setOption({
        title:{
            text:'ECharts 30天内数据实时更新'
        },
        xAxis:{
            type:"category",
            boundaryGap:false,
            data:date
        },
        yAxis:{
            boundaryGap:[0,'100%'],
            type:'value'
        },
        series:[{
            name:'成交',
            type:'line',
            smooth:true, //数据光滑过度
            symbol:'none', //下一个数据点
            stack:'a',
            areaStyle:{
                normal:{
                    color:'red'
                }
            },
            data:data
        }]
    })
    setInterval(function(){
        addData(true);
        myChart.setOption({
            xAxis:{
                data:date
            },
            series:[{
                name:'成交',
                data:data
            }]
        });
    },1000)
</script>
复制代码

效果展示

 

ECharts.js学习(二)动态数据绑定 – leoxuan – 博客园已关闭评论
2019年1月15日 By mikel 分类: JavaScript

来源: ECharts.js学习(一) 简单入门 – leoxuan – 博客园

EChart.js 简单入门

最近有一个统计的项目要做,在前端的数据需要用图表的形式展示。网上搜索了一下,发现有几种统计图库。

MSChart  

这个是Visual Studio里的自带控件,使用比较简单,不过数据这块需要在后台绑定。

ichartjs

是一款基于HTML5的图形库。使用纯javascript语言, 利用HTML5的canvas标签绘制各式图形。 支持饼图、环形图、折线图、面积图、柱形图、条形图等。

Chart.js

也是一款基于HTML5的图形库和ichartjs整体类似。不过Chart.js的教程文档没有ichartjs的详细。不过感觉在对于移动的适配上感觉比ichartjs要好一点。

ECharts.js

这是我准备在这个项目中使用的图形库,这也是一款基于HTML5的图形库。图形的创建也比较简单,直接引用Javascript即可。使用这个库的原因主要有三点,一个是因为这个库是百度的项目,而且一直有更新,目前最新的是EChart 3;第二个是这个库的项目文档比较详细,每个点都说明的比较清楚,而且是中文的,理解比较容易;第三点是这个库支持的图形很丰富,并且可以直接切换图形,使用起来很方便。

官网:ECharts.js

下面来简单说明一下EChart.js的使用。

第一步,引用Js文件

<script type="text/javascript" src="js/echarts.js"></script>

js文件有几个版本,可以根据实际需要引用需要的版本。下载链接

第二步,准备一个放图表的容器

<div id="chartmain" style="width:600px; height: 400px;"></div>

第三步,设置参数,初始化图表

复制代码
<script type="text/javascript">
        //指定图标的配置和数据
        var option = {
            title:{
                text:'ECharts 数据统计'
            },
            tooltip:{},
            legend:{
                data:['用户来源']
            },
            xAxis:{
                data:["Android","IOS","PC","Ohter"]
            },
            yAxis:{

            },
            series:[{
                name:'访问量',
                type:'line',
                data:[500,200,360,100]
            }]
        };
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));

        //使用制定的配置项和数据显示图表
        myChart.setOption(option);
    </script>
复制代码

这样简单的一个统计图表就出来了,官网使用的柱状图,我这边改用了折线图。

 

柱状图其实也很简单,只要修改一个参数就可以了。把series里的type 值修改为”bar”

饼图和折线图、柱状图有一点区别。主要是在参数和数据绑定上。饼图没有X轴和Y轴的坐标,数据绑定上也是采用value 和name对应的形式。

复制代码
        var option = {
            title:{
                text:'ECharts 数据统计'
            },            
            series:[{
                name:'访问量',
                type:'pie',    
                radius:'60%', 
                data:[
                    {value:500,name:'Android'},
                    {value:200,name:'IOS'},
                    {value:360,name:'PC'},
                    {value:100,name:'Ohter'}
                ]
            }]
        };
复制代码

 

ECharts.js学习(一) 简单入门 – leoxuan – 博客园已关闭评论
备案信息冀ICP 0007948