zoukankan      html  css  js  c++  java
  • (转载).net 缓存处理

    概述

    在ASP.NET应用程序构建过程中,为了提高应用程序的性能,缓存处理无疑是一个非常重要的环节。通常,我们将一些频繁被访问的数据,以及一些需要大量处理时间得出来的数据缓存在内存中,从而提高性能。例如,如果程序需要处理一张报表,这张报表的数据是关联的几张数据库表,并通过大量的计算得到的数据。我们知道表关联是比较耗时的,如果关联之后得出的数据再进行聚合排序等操作的话,那速度会更慢。因此,我们把查询的报表数据缓存起来,等下次用户再次请求时直接从内存中读取已经生成好的报表,这样对用户和程序无疑都是一件非常好的事情,用户减少了等待时间,程序减轻了压力。

    那么,何乐而不为呢,既然能让大家都开心的事情我们就去做吧。为此,ASP.NET提供了两种缓存方案。第一种是页输出缓存,它保存页处理输出,并在用户再次请求该页时,重用所保存的输出,而不是再次处理该页。第二种是应用程序缓存,它允许缓存您生成的数据,比如自定义报表对象,DataSet,DataTable等。但是有个问题就是ASP.NET为我们提供的缓存方案只能应用在单服务器中,如果我们的应用程序有几台服务器做负载均衡,或者我们做分布式应用,那么,ASP.NET为我们提供的缓存解决方案发挥的作用就不大了,我们需要其他的解决方案,现在比较成熟的缓存框架有Memcached,此框架用于分布式系统中,适用于Java,ASP.NET,PHP,Ruby等语言环境构建的应用程序。

    那么,下面就一一阐述以上提到的缓存方案。

    2、页输出缓存

    在页输出缓存中,ASP.NET为我们提供了两种解决方案,第一种是页面级输出缓存,第二种是片段缓存(用户控件缓存)。两种方案各有各的应用场景,我们来分别阐述。

    2.1、页面级输出缓存

    页面级输出缓存是比较简单的缓存形式,它是将响应请求而发送的HTML副本保存在内存中,当再有请求时直接将缓存的HTML输出给用户,直到缓存过期。这样,程序的性能会得到非常大的提升。

    实现

    具体的实现就非常简单了,只要页面顶部加一条OutputCache指令就可以了。

    <%@ OutputCache Duration="10" VaryByParam="none" %>

    它支持五个属性(Duration,VaryByParam,Location,VaryByCustom,VaryByHeader),有两个(Duration,VaryByParam)是必须的,我们也就研究这两个属性就可以了,也基本够我们日常使用。

    l Duration页面应该被缓存的时间,以为单位。必须是正整数。

    l VaryByParam Request 中变量的名称,这些变量名应该产生单独的缓存条目。"none" 表示没有变动。"*" 可用于为每个不同的变量数组创建新的缓存条目。变量之间用 ";" 进行分隔。

    l Location :指定应该对输出进行缓存的位置。如果要指定该参数,则必须是下列选项之一:AnyClientDownstreamNoneServer  ServerAndClient

    l VaryByHeader :基于指定的标头中的变动改变缓存条目。

    l VaryByCustom :允许在 global.asax 中指定自定义变动(例如,"Browser")。

    示例

    <1>.在Visual Studio .NET新建一个web项目,并且新建一个.aspx页面

    <2>.删除页面的上面的默认HTML代码

    <3>.把下面的代码COPY到刚新建的那个页面中

    <%@ OutputCache Duration="10" VaryByParam="none"%>

    <html>

    <head runat="server">

    <title>页面输出缓存示例</title>

    <script. type="text/C#" runat="server">

    void Page_Load(object sender, EventArgs e)

    {

    this.lblTime.Text = "Time:" + DateTime.Now.ToString();

    }

    </script>

    </head>

    <body>

    <strong>页面输出缓存示例</strong>

    <hr />

    <br />

    <asp:Label ID="lblTime" runat="server"></asp:Label>

    <br />

    <hr />

    <a href="opc.aspx?categoryid=test1">categoryid(test1)</a>

    <br />

    <a href="opc.aspx?categoryid=test2">categoryid(test1)</a>

    </body>

    </html>

    <4>.在浏览器中浏览此页面,您会页面上面的Time会有10秒的缓存,每过10秒,Time会变化一次,这时就是Duration="10"属性在起作用,因为我设置了缓存时间为10秒。好的,我们已经测试了Duration="10"属性。

    <5>.我们点击下面的categoryid(test1)和categoryid(test2)两个链接,发现Time是一样的,为什么呢?那是因为我们设置VaryByParam属性为none,我们之前解释过VaryByParam属性为none表示没有变动,意为保存一个缓存,适用于页面只有一个缓存的情况。那我们现在这样一个情况,有一个产品列表数据,其数据是根据产品的分类来决定显示哪些产品,所以我们这里的关键问题是为每个分类产品分别产生缓存。这时就需要用到VaryByParam属性了,它的用途我们已经知道了,现在我们把它的属性设置为categoryid,现在再试试分别点击两个链接,你就会看到两个链接的页面缓存不一样了。

    实战友情提示:

    l 切记,Duration 是用进行指定的。

    l 在使用 VaryByParam时,一定要注意 Request 变量大小写的变化会导致额外的缓存。比如刚才示例中categoryid=test1和categoryid=Test1会产生两个缓存版本,这里应用时要注意。

    2.2、片段缓存(用户控件缓存)

    对于页面级输出缓存的整页缓存方案,片段缓存是把页面的某个部分进行缓存,缓存一些很多页面所共有的页面部分,这样更节约内存资源,节约服务器压力,更符合面向对象的特点(封装)。比如页面的页头和页尾,很多页面都是公用相同的页头和页尾。再比如菜单部分,很多页面也是公用的一个菜单。这样,我们就可以一处缓存,多处使用。 像这样类似的场景我们就可以用片段缓存来实现。

    实现

    片段缓存的使用语法和页面输出缓存基本一样,但其应用于用户控件(.ascx文件),而页面输出缓存是应用于页面(.aspx文件)。对于其属性,它支持页面级输出缓存(除了Location属性)所有属性。并且用户控件还支持VaryByControl属性,该属性将根据用户控件(通常为用户控件页面上的控件,比如dropdownlist)成员的值改变而改变该控件的缓存。如果指定了VaryByControl,可以省略VaryByParam。

    在默认情况下,对每一个页面上面引用的每个用户控件都是单独缓存的。如果一个用户控件不随应用程序中的页面改变而改变,并且在所有的页面中使用相同的名称(ID相同),且使用了Shared="true"参数,那么所有引用该用户控件的缓存版本都是一样的。

    示例

    <1>. 借用页面级输出缓存建立的WEB项目,新建一个用户控件(.ascx文件)

    <2>. 把以下代码COPY到刚才新建的.ascx的页面文件中

    <%@ OutputCache Duration="10" VaryByControl="ddlcity" Shared="true" %>

    <script. type="text/C#" runat="server">

    protected void Page_Load(object sender, EventArgs e)

    {

    this.lblUCTime.Text = "usercontrol time:"+ DateTime.Now.ToString();

    }

    </script>

    <asp:DropDownList ID="ddlcity" runat="server" AutoPostBack="True">

    <asp:ListItem Value="1">北京</asp:ListItem>

    <asp:ListItem Value="2">江苏</asp:ListItem>

    <asp:ListItem Value="3">上海</asp:ListItem>

    <asp:ListItem Value="3">南京</asp:ListItem>

    </asp:DropDownList>

    <br />

    <hr />

    <br />

    <asp:Label ID="lblUCTime" runat="server"></asp:Label>

    <br />

    <3>. 新建一个.aspx页面,并删除页面文件中的HTML代码,把以下代码复制到页面文件中(注意顶部.cs文件引用别删除,有下划线的需要您替换)

    <%@ Register Src="您的用户控件.ascx" TagName="ucc" TagPrefix="uc1" %>

    <html >

    <head runat="server">

    <title>片段缓存示例</title>

    <script. type="text/C#" runat="server">

    protected void Page_Load(object sender, EventArgs e)

    {

    this.lblSelfTime.Text = "self time:" + DateTime.Now.ToString();

    }

    </script>

    </head>

    <body>

    <form. id="form1" runat="server">

    <strong>片段缓存示例</strong>

    <br />

    <hr />

    <uc1:ucc ID="Ucc1" runat="server" />

    <br />

    <hr />

    <asp:Label ID="lblSelfTime" runat="server"></asp:Label>

    </form>

    </body>

    </html>

    <4>. 再新建一个.aspx文件,并删除页面文件中的HTML代码,把以下代码复制到页面文件中(注意顶部.cs文件引用别删除,有下划线的需要您替换)

    <%@ Register Src="您的用户控件.ascx" TagName="ucc" TagPrefix="uc1" %>

    <html >

    <head id="Head1" runat="server">

    <title>片段缓存示例2</title>

    <script. type="text/C#" runat="server">

    protected void Page_Load(object sender, EventArgs e)

    {

    this.lblSelfTime.Text = "self 2 time:" + DateTime.Now.ToString();

    }

    </script>

    </head>

    <body>

    <form. id="form1" runat="server">

    <strong>片段缓存示例2</strong>

    <br />

    <hr />

    <uc1:ucc ID="Ucc1" runat="server" />

    <br />

    <hr />

    <asp:Label ID="lblSelfTime" runat="server"></asp:Label>

    </form>

    </body>

    </html>

    <5>. 在浏览器中浏览您刚才新建的两个.aspx页面,会发现两个页面的用户控件的缓存是一样的(usercontrol time 是同时变化的),当您选择城市列表时,会发现用户控件为每一个城市都缓存了一个版本。不同页面的每个城市的缓存版本一样。

    如果需要每个页面缓存版本不一样,就不要设置Shared="true"参数。大家可以通过上面的示例自己测试测试。

    实战友情提示:

    l 如果想每个页面引用的用户控件的缓存版本一样,就必须设置Shared="true"参数,并且用户控件ID一样

    3、应用程序缓存

    页面级和用户控件级缓存的确是一种可以迅速而简便地提高站点性能的方法,但是在ASP.NET中,缓存的真正灵活性和强大功能是通过Cache (System.Web.Caching.Cache)对象提供的。使用 Cache对象,您可以存储任何可序列化的数据对象,基于一个或多个依赖项的组合来控制缓存条目到期的方式。这些依赖项可以包括自从项被缓存后经过的时间、自从项上次被访问后经过的时间、对文件和/或文件夹的更改以及对其他缓存项的更改,在略作处理后还可以包括对数据库中特定表的更改。

    实现

    Cache对象位于System.Web.Caching. Cache中,其提供了两种增加缓存的方法,Add()和Insert()方法,这两种方法都有多个重载,且两种方法唯一的区别就是Add()返回已缓存对象的引用,Insert()没有返回值。Cache对象还提供了删除缓存的Remove()方法。

    具体的缓存实践我这里提供了简易封装后一个缓存工具类。可以直接用于项目中(适用于ASP.NET 2.0项目)。

    using System;

    using System.Text;

    using System.Web.Caching;

    using System.Collections;

    using System.Collections.Generic;

    using System.Text.RegularExpressions;

    namespace DianPing001.Cache

    {

    public static class ObjectCache

    {

    private static System.Web.Caching.Cache cache;

    private static double _SaveTime;

    /// <summary>

    /// 缓存保存时间,以分钟计算,默认分钟

    /// </summary>

    public static double SaveTime

    {

    get { return _SaveTime; }

    set { _SaveTime = value; }

    }

    static ObjectCache()

    {

    cache = System.Web.HttpContext.Current.Cache;

    _SaveTime = 30.0;

    }

    /// <summary>

    /// 获取缓存对象

    /// </summary>

    /// <param name="key">key</param>

    /// <returns>object</returns>

    public static object Get(string key)

    {

    return cache.Get(key);

    }

    /// <summary>

    /// 获取缓存数据,需要传入类型

    /// </summary>

    public static T Get<T>(string key)

    {

    object bj = Get(key);

    if (obj == null)

    {

    return default(T);

    }

    else

    {

    return (T)obj;

    }

    }

    /// <summary>

    /// 插入对象到缓存中

    /// </summary>

    /// <param name="key">key</param>

    /// <param name="value">对象</param>

    /// <param name="dependency">对象依赖</param>

    /// <param name="priority">优先级</param>

    /// <param name="callback">缓存删除时的回调事件</param>

    public static void Insert(string key, object value, CacheDependency dependency, CacheItemPriority priority, CacheItemRemovedCallback callback)

    {

    cache.Insert(key, value, dependency, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(SaveTime), priority, callback);

    }

    /// <summary>

    /// 插入对象到缓存中

    /// </summary>

    /// <param name="key">key</param>

    /// <param name="value">对象</param>

    /// <param name="dependency">对象依赖</param>

    /// <param name="callback">缓存删除时的回调事件</param>

    public static void Insert(string key, object value, CacheDependency dependency, CacheItemRemovedCallback callback)

    {

    Insert(key, value, dependency, CacheItemPriority.Default, callback);

    }

    /// <summary>

    /// 插入对象到缓存中

    /// </summary>

    /// <param name="key">key</param>

    /// <param name="value">对象</param>

    /// <param name="dependency">对象依赖</param>

    public static void Insert(string key, object value, CacheDependency dependency)

    {

    Insert(key, value, dependency, CacheItemPriority.Default, null);

    }

    /// <summary>

    /// 插入对象到缓存中

    /// </summary>

    /// <param name="key">key</param>

    /// <param name="value">对象</param>

    public static void Insert(string key, object value)

    {

    Insert(key, value, null, CacheItemPriority.Default, null);

    }

    /// <summary>

    /// 获取所有缓存对象的key

    /// </summary>

    /// <returns>返回一个IList对象</returns>

    public static IList<string> GetKeys()

    {

    List<string> keys = new List<string>();

    IDictionaryEnumerator cacheItem = cache.GetEnumerator();

    while (cacheItem.MoveNext())

    {

    keys.Add(cacheItem.Key.ToString());

    }

    return keys.AsReadOnly();

    }

    /// <summary>

    /// 删除缓存对象

    /// </summary>

    /// <param name="key">key</param>

    public static void Remove(string key)

    {

    cache.Remove(key);

    }

    /// <summary>

    /// 删除全部缓存

    /// </summary>

    public static void RemoveAll()

    {

    IList<string> keys = GetKeys();

    foreach (string key in keys)

    {

    cache.Remove(key);

    }

    }

    public static IList<string> RegexSearch(string pattern)

    {

    List<string> keys = new List<string>();

    IDictionaryEnumerator cacheItem = cache.GetEnumerator();

    while (cacheItem.MoveNext())

    {

    if (Regex.IsMatch(cacheItem.Key.ToString(), pattern))

    {

    keys.Add(cacheItem.Key.ToString());

    }

    }

    return keys.AsReadOnly();

    }

    /// <summary>

    /// 删除符合正则条件的cache

    /// </summary>

    /// <param name="pattern">条件</param>

    public static void RegexRemove(string pattern)

    {

    IList<string> keys = RegexSearch(pattern);

    foreach (string key in keys)

    {

    cache.Remove(key);

    }

    }

    }

    }

    具体的使用场景

    l 添加缓存

    /// <summary>

    /// 获取全部友情链接

    /// </summary>

    /// <returns></returns>

    public List<Links> GetAll()

    {

    List<Links> linksList = ObjectCache.Get<List<Links>>("c_Links_ALL"); [stone1]

    if (linksList == null)

    {

    linksList = ProviderManager.Factory.Links.GetAll();

    ObjectCache.Insert("c_Links_ALL",linksList);

    }

    [stone2] return linksList;

    }

    l 删除缓存

    /// <summary>

    /// 删除友情链接

    /// </summary>

    /// <param name="id"></param>

    /// <returns></returns>

    public int Delete(int id)

    {

    int retVar = ProviderManager.Factory.Links.Delete(id);

    if (retVar > 0)

    {

    ObjectCache.RegexRemove("c_Links*");[stone3]

    }

    return retVar;

    }

    到这里,已经介绍了ASP.NET为我们提供的缓存方案,从简单的页面级和用户控件缓存,到功能强大、可灵活定制的Cache对象。这些已经基本满足我们日常的需求。当然,缓存的强大之处还需要我们在实战中慢慢体会。

    4、分布式缓存

    在ASP.NET中已经为我们提供了一些缓存方案,但是如果我们需要搭建分布式缓存系统的话,ASP.NET提供的方案就不够用了。因此我们需要其他的解决方案。寻觅的一段时间后,发现一个叫Memcached的用于分布式系统的缓存方案。如果你想快速搭建性能卓越,功能强大的分布式系统,那Memcached绝对是您不二的选择。

    1) Memcached是什么?

    memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 为首开发的一款软件。许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。

    这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。

    2) Memcached能缓存什么?

    通过在内存里维护一个统一的巨大的hash表,Memcached能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。

    3) Memcached快吗?

    非常快,必须要介绍它的内部实现原理,只要知道有哪些站点在应用就可以了。Memcached已经成为mixi、hatena、Facebook、Vox、LiveJournal等众多服务中提高Web应用扩展性的重要因素。所以我们应该有理由相信Memcached的性能。

    4) Memcached特点

    memcached作为高速运行的分布式缓存服务器,具有以下的特点。

    • 协议简单

    • 基于libevent的事件处理

    • 内置内存存储方式

    • memcached不互相通信的分布式

    5) WindowsMemcached的安装使用

    a) 安装Memcached Server

    u 下载memcached的windows稳定版,解压放某个盘下面,比如在d:memcached

    u 在CMD下输入 "d:memcachedmemcached.exe -d install" 安装.

    u 再输入:"d:memcachedmemcached.exe -d start" 启动。

    备注:以后memcached将作为windows的一个服务每次开机时自动启动。这样服务器端已经安装完毕了。有几台缓存机器就为这些机器分别安装Memcached服务。

    安装常用设置:

    -p <num> 监听的端口

    -l <ip_addr> 连接的IP地址, 默认是本机

    -d start 启动memcached服务

    -d restart 重起memcached服务

    -d stop|shutdown 关闭正在运行的memcached服务

    -d install 安装memcached服务

    -d uninstall 卸载memcached服务

    -u <username> 以<username>的身份运行 (仅在以root运行的时候有效)

    -m <num> 最大内存使用,单位MB。默认64MB

    -M 内存耗尽时返回错误,而不是删除项

    -c <num> 最大同时连接数,默认是1024

    -f <factor> 块大小增长因子,默认是1.25

    -n <bytes> 最小分配空间,key+value+flags默认是48

    -h 显示帮助

    b) 使用Memcached .NET客户端

    u 下载Memcached的.NET客户端(C#)

    u 在项目中引用Enyim.Caching.dll文件

    u 添加配置文件,WEB项目为web.config,客服端软件项目为App.config,配置代码为

    <configuration>

    <configSections>

    <sectionGroup name="enyim.com">

    <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />

    </sectionGroup>

    <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />

    </configSections>

    <enyim.com>

    <memcached>

    <servers>

    <!-- put your own server(s) here-->

    <add address="127.0.0.1" port="11211" />

    <add address="192.168.111.189" port="11212" />

    </servers>

    <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" />

    </memcached>

    </enyim.com>

    <memcached keyTransformer="Enyim.Caching.TigerHashTransformer, Enyim.Caching">

    <servers>

    <add address="127.0.0.1" port="11211" />

    <add address="192.168.111.189" port="11212" />

    </servers>

    <socketPool minPoolSize="2" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" />

    </memcached>

    </configuration>

    u 测试代码

    using System;

    using System.Collections.Generic;

    using System.Text;

    using Enyim.Caching;

    using Enyim.Caching.Memcached;

    using Enyim.Caching.Configuration;

    using System.Net;

    namespace TestMemcached1

    {

    class Program

    {

    static void Main(string[] args)

    {

    string flag = "1"; //输入TAG,为插入缓存,为读取缓存数据,为删除缓存数据

    string key = "";

    string value = "";

    MemcachedClient mc = new MemcachedClient();

    Console.WriteLine("请输入操作类型(1插入缓存,读取缓存数据,删除缓存数据)...");

    while((flag = Console.ReadLine().Trim()) != "")

    {

    switch (flag)

    {

    case "1": {

    Console.WriteLine("请输入要插入缓存的KEY:");

    key = Console.ReadLine().Trim();

    Console.WriteLine("请输入与KEY对应的值:");

    value = Console.ReadLine().Trim();

    if (mc.Store(StoreMode.Set, key, value))

    {

    Console.WriteLine("{0}的值({1})插入成功",key,value);

    }

    }; break;

    case "2": {

    Console.WriteLine("请输入要删除的缓存的KEY");

    key = Console.ReadLine().Trim();

    if (mc.Get(key) == null)

    {

    Console.WriteLine("SORRY,{0}的值不存在", key);

    }

    else

    {

    if (mc.Remove(key))

    {

    Console.WriteLine("删除缓存({0})成功", key);

    }

    else

    {

    Console.WriteLine("删除缓存({0})失败", key);

    }

    }

    }; break;

    case "0": {

    Console.WriteLine("请输入要读取缓存数据的KEY");

    key = Console.ReadLine().Trim();

    if (mc.Get(key) == null)

    {

    Console.WriteLine("SORRY,{0}的值不存在", key);

    }

    else

    {

    Console.WriteLine("{0}的缓存数据为{1}",key,mc.Get(key));

    }

    }; break;

    default: Console.WriteLine("谢谢使用"); break;

    }

    }

    }

    }

    }

    c) 运行结果

    我是配置了两台服务器,本机和局域网内的一台机器,从配置文件中也可以看出具体配置了几台机器。

    其运行结果也是非常让人满意的,我在本机添加的缓存,在192.168.111.189那台机器上面可以查询到刚刚添加的缓存,同样在189机器添加的缓存,我本机同样可以查询,当然删除也是同步的。

    d) 查看Memcached运行情况

    使用telnet IP 端口 然后使用stats命令查看Memcached运行情况

    e) 实战友情提示:

    Memcached在修改服务端口时发现CMD下的修改命令并不起效果。后来发现在安装好的Memcached服务的启动项中并没有端口设置(默认值为11211),于是想到进注册表修改其服务启动参数。打开注册表,按照路径HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesmemcached Server,找到其中的ImagePath项,其值为:"d:memcachedmemcached.exe" -d runservice,将值修改为:"d:memcachedmemcached.exe" -p 80080 -m 256 -d runservice。重启服务后你就会发现该服务的端口已经变为80080了,-m 256表示设置了256M内存。

    5、小结

    到此为止,我们已经阐述了ASP.NET中各种缓存方案,并分享了一些实战经验。

    从最基本的页面级和用户控件级简单缓存,到高灵活性、高性能的Cache缓存对象,以及功能强大、性能卓越的分布式缓存系统Memcached,我们都已经有所了解、有所深入。那么在实践过程中,我们应该根据自己的实际需要去选择具体的缓存方案。比如单服务器中,我们可以选择Cached缓存对象实现缓存,一些长期不做修改的动态页面我们可以选择页面级缓存,多页面公用的菜单我们可以选择用户控件缓存。分布式系统我们就可以选择Memcached解决方案。

    有了这么多的缓存方案,让我们构建高性能的应用程序吧!


    [stone1]从缓存中读取数据并复制给对象列表

    [stone2]如果未能从缓存中获取到数据,就先从数据库中读取,然后把对象添加到缓存中

    [stone3]批量删除缓存(key中含有c_Links的缓存)

    转载来自:http://blog.itpub.net/183242/viewspace-700850/

  • 相关阅读:
    怎样用ZBrush中的Curves和Insert笔刷创建四肢
    如何利用ZBrush中的DynaMesh创建身体(二)
    如何利用ZBrush中的DynaMesh创建身体(一)
    如何用ZBrush雕刻出栩栩如生的头发(二)
    Fisker大师用ZBrush制作兽人萨尔全过程
    如何用ZBrush雕刻出栩栩如生的头发(一)
    ZBrush中的SubTool工具该怎样使用
    Access denied for user 'Administrator'@'localhost' (using password: YES)
    java.lang.NoClassDefFoundError: org/apache/ibatis/session/SqlSession
    Unable to install breakpoint in
  • 原文地址:https://www.cnblogs.com/loushuibazi/p/4340564.html
Copyright © 2011-2022 走看看