(接上文《架构设计:系统存储(20)——图片服务器:需求和技术选型(2)》)
图片服务系统工程的示例代码放置在 http://download.csdn.net/detail/yinwenjie/9740380 这里,读者可以自行进行下载。
根据前文对图片服务系统的系统架构规划,Nginx 充当了第三级缓存的作用和对图片请求服务的负载均衡作用,所以在 Nginx 配置文件中,至少需要对 Nginx Proxy Cache 功能和 UpStream 功能进行配置。以下为主要的 Rewrite 部分和负载均衡部分的配置:
- ......upstream imageserver {
- server 192.168.61.1 : 8080;
- server 192.168.61.2 : 8080;#建议配置健康检查部分
- }......server {......location / imageQuery {#后续还要增加proxy cache部分的配置#cluster loader proxy_pass http: //imageserver;
- }......
- }
- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
请记得按照我们在负载均衡专题中讨论的 Nginx 优化细节进行其它配置信息的调整工作《架构设计:负载均衡层设计方案(2)——Nginx 安装》,以下配置信息是和 Proxy Cache 相关的配置信息:
- ......proxy_cache_path / nginxcache / imagecache levels = 1 : 2 keys_zone = imagecache: 500m inactive = 300s max_size = 1g;
- server {......#带有特效参数的location~ * ^/image/ (. * )\. (jpg | jpeg | png | gif) & (. * ) $ {#rewrite rewrite ^ /image/ (. * )\. (jpg | jpeg | png | gif) & (. * ) $ / imageQuery / $1.$2 ? special = $3 last;
- }
- #不带有特效参数的location~ * ^/image/ (. * )\. (jpg | jpeg | png | gif) $ {#rewrite rewrite ^ /image/ (. * )\. (jpg | jpeg | png | gif) $ / imageQuery / $1.$2 last;
- }
- #重写后的访问路径location / imageQuery {#proxy cache proxy_cache imagecache;
- proxy_cache_valid 200 304 300s;
- proxy_cache_key $uri$query_string;
- #cluster loader proxy_pass http: //imageserver;
- }......
- }
- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
为了便于客户端拼凑字符串,图片服务系统向客户端提供了一个约定俗成的 url 地址结构,然客户端可以在原始图片的 url 后面使用 "&" 符号给出要求处理的图片特效,类似如下的结构
- http: //imagenginxproxy/image/20170114/e1733711-8e4d-4d9e-b230-dd22898313ef.png&zoomimage|ratio=0.9->markimage|markValue=www.yinwenjie.net111
- 1 1
当 Nginx 收到这个请求后,将会 rewrite 这个 url 到图片服务层的真实 url 地址,并且将请求得到的数据通过 Proxy Cache 进行缓存。关于 upstream 功能的配置和 rewrite 功能的配置,我们已经在负载均衡专题中进行了讲解,这里就不再赘述了。这里主要再说明一下 Proxy Cache 的一些基本配置(关于 Proxy Cache 模块的更多配置项,可参考官方文档:http://nginx.org/en/docs/http/ngx_http_proxy_module.html。):
- .....
- #http200和302的缓存时间为5分钟
- proxy_cache_valid 200 302 5m;
- #http 404的缓存时间为4分钟
- proxy_cache_valid 404 4m;....1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
但是作为图片服务的上层缓存模块来说,我们不能这样进行设置。这是因为同一张图片的不同特效要求,其 URL 地址都是一样的,只是参数不一样而已。否则同一张图片的不同显示效果,将会被 Nginx 缓存视为统一返回数据:
- proxy_cache_key $uri 1 1
正确的设置方式,是必须指定 Proxy Cache 模块在进行 Key 计算时,要参考 URL 的参数设置。所以以下设置方式才是正确的:
- 以下两个URL代表的特效,将会出现缓存错误 / imageQuery / 20170114 / e1733711 - 8e4d - 4d9e - b230 - dd22898313ef.png ? special = zoomimage | ratio = 0.55 / imageQuery / 20170114 / e1733711 - 8e4d - 4d9e - b230 - dd22898313ef.png ? special = zoomimage | ratio = 0.9 1 2 3 1 2 3
- ......
- #其中$query_string,就代表URL中的参数信息
- proxy_cache_key $uri$query_string;......1 2 3 4 5 6 1 2 3 4 5 6
前文我们还提到,我们将基于责任链模式以生产线的方式,进行客户端要求的特效处理过程。本小节我们对这部分主要的代码进行说明。在示例的工程中,我们已经实现了三个图片特效:图片等比例缩放操作、图片裁剪操作和字符串性质的水印操作:
上图给出的类图中,除了前文介绍的责任链模式外还有一个创建者用来简化整个生产线的构建过程。创建者模式在很多组件中都有使用,例如 Protobuf 中创建对象所使用的手段就是先创建一个创建者,然后再由这个创建者进行实际对象的创建:
- ......
- // Protobuf中的实力创建:
- MessageS.Message.Builder messageBuilder = MessageS.Message.newBuilder();
- // 用户名
- messageBuilder.setUserName(username);
- // 商品id
- messageBuilder.setBusinessCode(buid);
- // 等一系列其它属性值
- ......
- // 再进行实际对象的创建
- MessageS.Message messagePB = messageBuilder.build();......1 2 3 4 5 6 7 8 9 10 11 12 1 2 3 4 5 6 7 8 9 10 11 12
以下是 ImageHandler 抽象类的部分定义,和 ZoomImageHandler 图片缩放处理器的重要代码片段:
- /**
- * 这个dispose方法,就是子类需要主要实现的方式。<br>
- * 如果处理过程中,不需要变更画布的尺寸,则可以在处理后将srcImage代表的画布直接返回<br>
- * 如果在处理过程中,需要变更画布尺寸,则可以在实现的方法中创建一个新的画布,并进行返回
- * @param srcImage 从上一个处理器传来的处理过得图片信息(画布信息)
- * @return 处理完成后一定要返回一个画布。
- */
- public abstract BufferedImage dispose(BufferedImage srcImage);
- 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
- ......public BufferedImage dispose(BufferedImage srcImage) {
- /*
- * 处理过程为
- * 1、首先确定外部输入的是按比例缩放还是按照一个高宽数值缩放
- * 2、如果是按照一个高宽数值缩放,则要首先计算一个缩放比例
- * 3、构建一个新的画布,并按照指定的比例或者计算出来的比例进行缩放操作
- * */
- int sourceWith = srcImage.getWidth();
- int sourceHeight = srcImage.getHeight();
- //得到合适的压缩大小,按比例。
- int localDestWith,
- localDestHeight;
- // 如果条件成立,说明是按照比例缩小
- if (ratio != -1) {
- localDestWith = Math.round((sourceWith * ratio));
- localDestHeight = Math.round((sourceHeight * ratio));
- }
- // 否则是按照输入的宽、高重新计算一个比例,再进行缩小
- else {
- float localRatio;
- // 如果发现输入的目标高宽大于图片的原始高宽,则按照ratio==1处理
- if (sourceWith <= this.destWith || sourceHeight <= this.destHeight) {
- localDestHeight = sourceHeight;
- localDestWith = sourceWith;
- }
- // 按照高计算
- else if (sourceWith > sourceHeight) {
- localRatio = new BigDecimal(this.destHeight).divide(new BigDecimal(sourceHeight), 2, RoundingMode.HALF_UP).floatValue();
- localDestHeight = (int)(sourceHeight * localRatio);
- localDestWith = (int)(sourceWith * localRatio);
- }
- // 否则按照宽计算
- else {
- localRatio = new BigDecimal(this.destWith).divide(new BigDecimal(sourceWith), 2, RoundingMode.HALF_UP).floatValue();
- localDestHeight = (int)(sourceHeight * localRatio);
- localDestWith = (int)(sourceWith * localRatio);
- }
- }
- // 快速缩放算法
- Image destImage = srcImage.getScaledInstance(localDestWith, localDestHeight, Image.SCALE_FAST);
- // RGB位深为24位,适合互联网显示
- BufferedImage outputImage = new BufferedImage(localDestWith, localDestHeight, BufferedImage.TYPE_INT_RGB);
- Graphics graphics = outputImage.getGraphics();
- graphics.drawImage(destImage, 0, 0, null);
- graphics.dispose();
- // 继续进行下一个处理
- BufferedImage nextResults = this.doNextHandler(outputImage);
- if (nextResults == null) {
- return outputImage;
- }
- return nextResults;
- }......1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
在这个示例的图片服务系统中,对于 Redis 的操作有两个关键点。首先 Redis 官方推荐的 Java 客户端实现 Jedis,提供了一个 redis.clients.jedis.JedisCluster 类对 Redis Cluster 进行操作。问题是这里面只提供了针对 String 类型的 Value 进行操作,读者可以查看 JedisCluster 类的源代码进行验证。还好 Jedis 中的这部分代码不难懂,那么要满足我们图片服务系统中对 byte 类型的 Value 进行操作的要求,我们可以仿照 JedisCluster 类的实现自行实现一个,分别是 IBinaryJedisCluster 接口和 BinaryJedisClusterImpl 实现。这里的代码我们不需要按照 JedisCluster 类中实现对 Redis 中所有数据接口的操作,只需要实现我们系统中需要的 Redis 操作即可:
- ......
- /**
- * Redis Cluster 客户端操作实现,这个类参考自JedisCluster而来<br>
- * 源自JedisCluster对JedisClusterCommand的封装只能对String类型的value进行处理
- * 不能对byte[]形式的value进行处理
- * @author yinwenjie
- */
- public class BinaryJedisClusterImpl implements IBinaryJedisCluster,
- Closeable {
- public static final short HASHSLOTS = 16384;
- private static final int DEFAULT_TIMEOUT = 2000;
- private static final int DEFAULT_MAX_REDIRECTIONS = 20;
- ......
- @Override public String set(final String key, final byte[] value, final String nxxx, final String expx, final long time) {
- return new JedisClusterCommand < String > (connectionHandler, maxRedirections) {@Override public String execute(Jedis connection) {
- return connection.set(key.getBytes(), value, nxxx.getBytes(), expx.getBytes(), time);
- }
- }.run(key);
- }
- @Override public byte[] get(final String key) {
- return new JedisClusterCommand < byte[] > (connectionHandler, maxRedirections) {@Override public byte[] execute(Jedis connection) {
- return connection.get(key.getBytes());
- }
- }.run(key);
- }
- ......
- @Override public Boolean exists(final String key) {
- return new JedisClusterCommand < Boolean > (connectionHandler, maxRedirections) {@Override public Boolean execute(Jedis connection) {
- return connection.exists(key);
- }
- }.run(key);
- }......
- }
- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
在前文的系统设计章节,我们还介绍到 Redis 对超过 10KB 的 Value 信息有较高的读写延迟,而我们存储到 Redis 中的图片信息又都比较大(20KB 是普片现象,200KB 也是可能的),在设计章节中提到的处理办法是将一个图片信息拆分为多个 Value 存储到 Redis Cluster 中的不同节点上。以下是进行 Value 存储时的主要代码片段:
- ......@Override public void saveCache(String imageURL, byte[] imagebytes) throws BusinessException {
- /*
- * 1、经过合法性验证后,处理的第一步就是判断imagebytes需要被分成几个段
- * 2、然后进行byte的拆分和保存
- * 3、只有所有的段都保存成功了,才进行返回(同步的,连续性的)
- * */
- if (StringUtils.isEmpty(imageURL) || imagebytes == null) {
- throw new BusinessException("参数错误,请检查!", BusinessCode._404);
- }
- // 1、======确定分段
- Integer maxLen = imagebytes.length;
- Integer splitNum = maxLen / IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
- Integer remainLen = maxLen % IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
- if (remainLen != 0) {
- splitNum++;
- }
- // 开始构造key
- String key[] = new String[splitNum];
- for (int index = 0; index < splitNum; index++) {
- key[index] = imageURL + "|" + index;
- }
- String lenkey = imageURL + "|size";
- ......
- // 保存长度信息到缓存系统
- binaryJedisCluster.set(lenkey, maxLen.toString().getBytes());
- for (int index = 0; index < splitNum; index++) {
- byte[] values = null;
- Integer valuesLen = null;
- // 确定本次要添加的分片长度
- if (index + 1 == splitNum) {
- valuesLen = remainLen == 0 ? IImageEffectsCacheService.PERPATCH_IMAGE_SIZE: remainLen;
- } else {
- valuesLen = IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
- }
- values = new byte[valuesLen];
- // 复制byte信息
- // System.arraycopy是操作系统级别的复制操作,比arraybyte stream快
- System.arraycopy(imagebytes, index * IImageEffectsCacheService.PERPATCH_IMAGE_SIZE, values, 0, valuesLen);
- binaryJedisCluster.set(key[index], values, "NX", "EX", redisProperties.getKeyExpiredTime());
- }
- }......1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
有的读者指出这里可以在 Redis Cluster Client 部分使用线程池,同时读取多个 Value 部分,并通过 CountDownLatch 控制所有 Value 读取完成后再进行合并。但实际上这种读取方式在 Redis Cluster 节点较少的时候对于单个 Client 来说意义不大。主要还是因为 Redis 服务节点工作在单线程状态下,完全依靠操作系统的多路复用 I/O 模型、自身实现的事件分离器、全内存态数据存储和内部的数据结构实现来保证吞吐性能。但是随着 Redis Cluster 中 Master 节点的增多,以上所描述的 Client 多线程方式就有性能优势了,这是因为通过 Jedis Client CRC16 Hash 算法,同一个图片的不同 byte 段会分配到更多不同的 Redis 节点上进行存储,这样就能够实现同一张图片不同 byte 段的同时读取了。
百度搜索 "就爱阅读", 专业资料, 生活学习, 尽在就爱阅读网 92to.com, 您的在线图书馆!
来源: http://www.92to.com/bangong/2017/07-26/25724891.html