Redis实现中间件_brycegao321的博客-CSDN博客_redis中间件

mikel阅读(1114)

来源: Redis实现中间件_brycegao321的博客-CSDN博客_redis中间件

Redis基本教程: http://www.runoob.com/redis/redis-tutorial.html

其实Redis很简单,就是充当缓存的作用, 不能替代MySQL(及其它)数据库。 做个比喻: 数据库就相当于硬盘,Redis就相当于内存, 在Redis里读写数据更快。 Redis一般作为中间件,服务将数据缓存到Redis里, 但必须要注意跟数据库的同步问题!!!

原则:先在Redis里查,如果查不到再去数据库查, 并保持结果到Redis里。

因为Redis使用关键字读写键值的, 如果多个团队使用同一个Redis服务, 那么很可能出现关键字冲突的问题。 解决办法有2个:

1、 为每个团队分配不同的关键字前缀, 例如 jkcom_user***,jkcom_sell***等。

2、 每个团队使用不同的database, 同一个database要保证key不同,不同database的key可以相同。 (PS

: 每个database都是独立的存储空间,  在redis.conf配置,默认有16个,即SELECT 0~SELECT 15。)

 

  1. # Set the number of databases. The default database is DB 0, you can select
  2. # a different one on a per-connection basis using SELECT <dbid> where
  3. # dbid is a number between 0 and ‘databases’-1
  4. databases 16

 

 

在redis目录里执行./src/redis-server启动redis服务, 默认使用redis.conf配置文件。

执行./src/redis-cli连接到客户端。

API说明:http://docs.spring.io/spring-data/redis/docs/current/api/

Maven包最新代码地址:

spring-data-redis:  https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis
jedis:  https://mvnrepository.com/artifact/redis.clients/jedis

在SpringMVC的配置文件里添加redis的代码包, 版本号去文章上面仓库去查:
     创建redis的配置文件, Java代码会读取这个文件。 PS:在实际开发中, 要写多个配置文件分别对应开发、测试、预上线、线上环境等(通过配置springmvc的profiles标签切换)。
     这里有个坑: 如果要设置database, 必须先设置密码!!!
     下面代码可以拿到Jedis实例引用, 然后就可以调用它的增删改查方法了。
  1. public class RedisProvider {
  2. protected static JedisPool jedispool;
  3. static{
  4. ResourceBundle bundle = ResourceBundle.getBundle(“redis”); //读取redis.properties文件
  5. if (bundle == null) {
  6. throw new IllegalArgumentException(
  7. “[redis.properties] is not found!”);
  8. }
  9. try {
  10. JedisPoolConfig jedisconfig = new JedisPoolConfig();
  11. jedisconfig.setMaxIdle(Integer.valueOf(bundle
  12. .getString(“redis.pool.maxIdle”)));
  13. jedisconfig.setTestOnBorrow(Boolean.valueOf(bundle
  14. .getString(“redis.pool.testOnBorrow”)));
  15. jedisconfig.setTestOnReturn(Boolean.valueOf(bundle
  16. .getString(“redis.pool.testOnReturn”)));
  17. jedispool = new JedisPool(jedisconfig, bundle.getString(“redis.ip”),
  18. Integer.valueOf(bundle.getString(“redis.port”)),
  19. Integer.valueOf(bundle.getString(“redis.timeout”)),
  20. bundle.getString(“redis.password”),
  21. Integer.valueOf(bundle.getString(“redis.database”)));
  22. } catch (Exception ex) {
  23. ex.printStackTrace();
  24. }
  25. }
  26. public static Jedis getJedis() {
  27. Jedis jedis = null;
  28. try {
  29. jedis = jedispool.getResource();
  30. } catch (JedisConnectionException jce) {
  31. jce.printStackTrace();
  32. }
  33. return jedis;
  34. }
  35. }

编写最简单的读写字符串示例:

          OK, 成功!
          Redis还支持list,set,zset,hash等数据结构。
        实际生产环境至少部署主从redis服务器, 对于大数据量的服务要部署redis分布式集群(Codis,阿里redis集群或redis sentinel)。

Redis 与 MQ 的区别 - 逆水行舟,平原走马 - 博客园

mikel阅读(830)

来源: Redis 与 MQ 的区别 – 逆水行舟,平原走马 – 博客园

Redis是一个高性能的key-value数据库,它的出现很大程度补偿了memcached这类key-value存储的不足。虽然它是一个数据库系统,但本身支持MQ功能,完全可以当做一个轻量级的队列服务器使用。

不过,Redis只是提供一个高性能的、原子操作内存键值队,具有高速访问能力,虽可用做消息队列的存储,但是不具备消息队列的任何功能和逻辑,要作做为消息队列来实现的话,功能和逻辑要通过上层应用自己实现。

 

  Redis从2.0版本开始支持发布/订阅指令,发布者调用redis的publish方法往特定的channel发送消息,订阅者在初始化的时候要订阅到该channel,一旦有消息就会立即接收。
  Redis 消息推送(基于分布式 pub/sub)多用于实时性较高的消息推送,并不保证可靠。redis-pub/sub断电就清空,而使用redis-list作为消息推送虽然有持久化,但是也并非完全可靠不会丢。其他的mq和kafka保证可靠但有一些延迟(非实时系统没有保证延迟)。
  另外一点,redis 发布订阅除了表示不同的 topic 外,并不支持分组。但kafka是支持分组的,比如kafka中发布一个东西,多个订阅者可以分组,同一个组里只有一个订阅者会收到该消息,而这个可以用作负载均衡。

Redis发布订阅与rabbitmq的区别

1. 可靠性

redis :没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中;

rabbitmq:具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费。

2. 实时性

redis:实时性高,redis作为高效的缓存服务器,所有数据都存在内存中,所以它具有更高的实时性

3. 消费者负载均衡:

rabbitmq队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载;

redis发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者,她是一种消息的广播形式,redis本身不做消费者的负载均衡,因此消费效率存在瓶颈;

4. 持久性

redis:redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式(redis持久化方式,后续更新),可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。

rabbitmq:队列,每条消息都可以选择性持久化,持久化粒度更小,更灵活;

5. 队列监控

rabbitmq实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用;

redis没有所谓的监控平台。

6. 性能

性能上,对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。

总结

redis: 轻量级,低延迟,高并发,低可靠性;

rabbitmq:重量级,高可靠,异步,不保证实时;

rabbitmq是一个专门的AMQP协议队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而redis主要是用于缓存的,redis的发布订阅模块,可用于实现及时性,且可靠性低的功能。

 

 

Redis发布订阅与ActiveMQ的比较

  1. ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持;

  2. ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;

  3. ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。

总之,ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的。但是如果系统中已经有了Redis,并且只需要基本的发布订阅功能,可以考虑使用Redis的发布订阅机制以满足需求。

使用Redis实现MQ_沈林楠的专栏-CSDN博客

mikel阅读(1400)

来源: 使用Redis实现MQ_沈林楠的专栏-CSDN博客

要说明如何实现MQ之前,需要先说明一下MQ的分类,总共分为两类:

publish-subscribe

发布订阅模式有点类似于我们日常生活中订阅报纸。每年到年尾的时候,邮局就会发一本报纸集合让我们来选择订阅哪一个。在这个表里头列了所有出版发行的报纸,那么对于我们每一个订阅者来说,我们可以选择一份或者多份报纸。比如北京日报、潇湘晨报等。那么这些个我们订阅的报纸,就相当于发布订阅模式里的topic。有很多个人订阅报纸,也有人可能和我订阅了相同的报纸。那么,在这里,相当于我们在同一个topic里注册了。对于一份报纸发行方来说,它和所有的订阅者就构成了一个1对多的关系。这种关系如下图所示:
这里写图片描述

Producer-Consumer

Producer-Consumer的过程则理解起来更加简单。它好比是两个人打电话,这两个人是独享这一条通信链路的。一方发送消息,另外一方接收,就这么简单。在实际应用中因为有多个用户对使用p2p的链路,它的通信场景如下图所示:
这里写图片描述

Redis中的publish-subscribe

redis中已经实现了publish-subscribe,订阅者(Subscriber)可以订阅自己感兴趣的频道(Channel),发布者(Publisher)可以将消息发往指定的频道(Channel),正式通过这种方式,可以将消息的发送者和接收者解耦。另外,由于可以动态的Subscribe和Unsubscribe,也可以提高系统的灵活性和可扩展性。
打开redis客户端,使用SUBSCRIBE命令就可以订阅消息了,如:

SUBSCRIBE china hongkong

发布命令如下:

PUBLISH china "hahahaha"

这样在消息订阅的一方就可以接收到消息了,如下:

1) "message"
2) "china"
3) "hahahaha"

 

要想取消订阅可以使用:

UNSUBSCRIBE china hongkong

上面是如何使用redis客户端进行消息的订阅和发布,下面介绍一下如何使用代码实现,我们目前使用Spring Boot的工程框架,所以很多东西不需要手工去配置了,默认Spring Boot会帮我们实现RedisTemplate的bean,所以我们直接注入使用即可。

@Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new PatternTopic("chat"));
        return container;
    }

@Bean
MessageListenerAdapter listenerAdapter(Receiver receiver) {
    return new MessageListenerAdapter(receiver, "receiveMessage");
}

这里的代码的意思是将消息接收的处理方法和我们的redis订阅端进行一个连接。

return new MessageListenerAdapter(receiver, "receiveMessage");

这里就是接收消息的对象和方法,以后要扩展的话,可以做一个接口,可能通过不同的tag或者是其他的标志,来使用不同的对象处理消息。

container.addMessageListener(listenerAdapter, new PatternTopic("chat"));

代码这里也可以做成接收多个消息的topic,也是需要重构代码的。
使用RedisTemplate的convertAndSend方法就可以发送消息了,如下:

redisTemplate.convertAndSend("chat", "Hello from Redis!");

至此,redis的消息发布订阅就介绍完了

Redis中的Producer-Consumer

对于如何实现Producer-Consumer,redis并没有比较直接的方案,但是在list中提供了一个方法RPOPLPUSH,其中官方的资料是这样介绍的:

模式:安全的队列
Redis通常都被用做一个处理各种后台工作或消息任务的消息服务器。 一个简单的队列模式就是:生产者把消息放入一个列表中,等待消息的消费者用 RPOP 命令(用轮询方式), 或者用 BRPOP 命令(如果客户端使用阻塞操作会更好)来得到这个消息。
然而,因为消息有可能会丢失,所以这种队列并是不安全的。例如,当接收到消息后,出现了网络问题或者消费者端崩溃了, 那么这个消息就丢失了。
RPOPLPUSH (或者其阻塞版本的 BRPOPLPUSH) 提供了一种方法来避免这个问题:消费者端取到消息的同时把该消息放入一个正在处理中的列表。 当消息被处理了之后,该命令会使用 LREM 命令来移除正在处理中列表中的对应消息。
另外,可以添加一个客户端来监控这个正在处理中列表,如果有某些消息已经在这个列表中存在很长时间了(即超过一定的处理时限), 那么这个客户端会把这些超时消息重新加入到队列中。

首先说明了,为什么会有这个命令,就是因为在使用RPOP或者BRPOP命令的时候,会出现丢失的问题,所以需要在从一个队列弹出的时候立马将这个对象放到工作队列中,等完成之后再进行删除操作。

在实际的使用中,我们使用的是RPOPLPUSH的阻塞版,也就是说,在没有获取到消息的时候,这个获取的任务会一直阻塞在线程中,直到从队列中取出消息为止。

到目前为止,已经将理论介绍完毕了,下面就说说代码是如何实现的。

String recieveQueueMessage = redisTemplate.opsForList().rightPopAndLeftPush(waitQueue, workQueue, 0, TimeUnit.MILLISECONDS);

这是最核心的代码部分,使用的是RedisTemplate中用来操作list的接口rightPopAndLeftPush,他是将waitQueue列表最底部的信息弹出,推送到workQueue顶部,等待执行,如果执行都没有问题,再使用

redisTemplate.opsForList().remove(workQueue, REMOVE_COUNT, messageQueueEntity);

代码进行删除工作队列的操作,如果没有弹出信息,则继续进行等待,第一个参数是要移出的队列,第二个参数是移出的数目,第三个参数是要移出的内容。

那整体是如何进行工作的呢,下面贴一下整体的代码,然后再详细的进行说明:

@PostConstruct
public void init() {
    executorService = Executors.newFixedThreadPool(threadCount);
    LOGGER.info("INIT|RECIEVE|MESSAGE|START...");
    for(int i = 0; i < threadCount; i++){
        executorService.execute(() -> {
                    String threadName = Thread.currentThread().getName();
                    while(true) {
                        MessageQueueEntity message = channelAdapter.getMessage();
                        LOGGER.info("RECIEVE|MESSAGE|SUCCESS|{}|{}|", threadName, message);
                        LOGGER.info("START|HANDLE|MESSAGE|{}", message.getId());
                        try{
                            smsSendService.sendSms(message);
                        } catch(SmsSendErrorException e) {
                            LOGGER.error("SENDSMS|ERROR|{}|{}", message.getId(), e);
                        } catch(Exception e) {
                            e.printStackTrace();
                            LOGGER.error("SENDSMS|UNKNOW|ERROR|{}|{}", message.getId(), e);
                        }
                        LOGGER.info("FINISH|HANDLE|MESSAGE|{}", message.getId());
                    }
                }

        );
    }
}

@PreDestroy
public void destroy() {
    executorService.shutdown();
    LOGGER.info("SHUTDOWN|RECIEVE|MESSAGE|SUCCESS|");
}
  1. 可以看到使用了spring注解@PostConstruct和@PreDestroy,@PostConstruct注解是要在bean注入的时候去初始化的方法上的,所以当bean进行spring的注入之后,里面的内容就会自动的执行,因为我们要接收信息的时机必须是在启动服务器之后自动就执行,所以使用了这两个注解。
  2. 使用了Executors.newFixedThreadPool(threadCount)多线程,这里是固定产生threadCount个线程的线程池,无论是否使用,线程都会等待在那里,threadCount是根据配置来生成了,为了以后能够进行很好的扩展。
  3. for(int i = 0; i < threadCount; i++)这里的循环是有几个线程就要执行几次。
  4. 后面是比较核心的部分,while(true)可以保证在服务器启动到结束这之间,这几个线程一直在运行,并接收着信息。
  5. 接收之后就是之前讲过的使用redis的方式来进行队列的操作
  6. 这里值得一提的是,无论多少个线程,多少个消息,他们都是轮询的。

解决:SqlDateTime 溢出。必须介于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM 之间提示问题 - JiYF - 博客园

mikel阅读(784)

来源: 解决:SqlDateTime 溢出。必须介于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM 之间提示问题 – JiYF – 博客园

提示信息如下

“/”应用程序中的服务器错误。


SQLDateTime 溢出。必须介于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM 之间。

问题现象:

 

问题原因:

出现个问题的原因是:在更新或者添加数据,出现的错误,再给parameters传递值没有对DateTime类型字段没有传递值,默认为null

但是在SQLServer里面对DateTime类型取值范围是:介于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM 

 但是在而.NET Framework中,DateTime类型,最小值是1/1/0001 0:00:00  12/31/9999 11:59:59 PM

当在传递数据时候,没有给DateTime类型赋值,默认为null即:1/1/0001 0:00:00 显然不在SQLServer中Datetime类型的范围之中,就产生溢出,导致此错误信息

代码查找:

字段

 

数据添加

数据库中的datetime类型字段:

 

解决办法:

办法1.再给datetime类型得变量赋值:不让其等于null而且在区间于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM这个之间

办法2:使用System.Data.SQLTypes.SQLDateTime.MinValue替代System.DateTime类型,这样SqlDateTime的MinValue和Sql中DateTime的范围吻合,就不会再出现以上的错误了。

layui 框架 table插件 实现键盘快捷键 切换单元格编辑 - zakary_zhen - 博客园

mikel阅读(1139)

来源: layui 框架 table插件 实现键盘快捷键 切换单元格编辑 – zakary_zhen – 博客园

最近使用layui的框架时,发现table插件不支持键盘快捷键切换单元格,花了点时间实现此功能。

分享给有需要的朋友们~~~

 

效果图

 

代码:

 

1.支持 enter,上,下,右键 切换单元格,支持隐藏列跳过切换。

注:单元格必须开启了 edit:text 模式,才支持键盘切换。

使用方法:
1.在需要启用此功能的页面中table done回调函数中插入。
2.修改源代码,在源代码中直接插入,此方法就不需要在每一个页面的table done回调函数中 插入。

layui table 可编辑单元格 JS实现通过键盘上下左右键 光标焦点移动到旁边的编辑行/列_CaiXinXing的CSDN博客-CSDN博客

mikel阅读(2618)

来源: layui table 可编辑单元格 JS实现通过键盘上下左右键 光标焦点移动到旁边的编辑行/列_CaiXinXing的CSDN博客-CSDN博客

layui table 可编辑单元格 JS实现通过键盘上下左右键 光标焦点移动到旁边的编辑行/列

类似效果

上下左右键实现的代码

layui.config({
base: ‘/static/layuiadmin/’ //静态资源所在路径
}).extend({
index: ‘lib/index’ //主入口模块
}).use([‘index’,’table’,’form’],function(){

var $ = layui.JQuery;
var table = layui.table,
var form = layui.form;

//按键监听事件
$(document).on(‘keydown’, ‘.layui-input’,
function(event) {
var td = $(this).parent(‘td’);
var index = td.index();
var tr = td.parent(‘tr’);
switch (event.key) {
case “ArrowUp”://上键
tr[‘prev’]().children(‘td’).eq(index).click();
break;
case “ArrowDown”://下键
tr[‘next’]().children(‘td’).eq(index).click();
break;
case “ArrowLeft”://左键
td[‘prevAll’](‘[data-edit=”text”]:first’).click();
break;
case “ArrowRight”://右键
td[‘nextAll’](‘[data-edit=”text”]:first’).click();
break;
}
});

}

layui form表单 input输入框获取焦点后 阻止Enter回车自动提交 - &执念 - 博客园

mikel阅读(2549)

来源: layui form表单 input输入框获取焦点后 阻止Enter回车自动提交 – &执念 – 博客园

最简单的解决办法,不影响其他操作,给提交按钮增加 type=”button” 属性 完美解决

<button type="button" class="layui-btn" lay-submit lay-filter="*">立即提交</button>

layui表格编辑状态下回车进入下一单元格

$(document).keyup(function (event) {
if (event.keyCode == “13”) {
if ($($(“.layui-table-edit”).parent().next()).length > 0) {
$($(“.layui-table-edit”).parent().next()).click();
} else {
if ($(“.layui-table-edit”).parent().parent().next().find(“td[data-field=’OldMeterNo’]”).length > 0) {
$($(“.layui-table-edit”).parent().parent().next().find(“td[data-field=’OldMeterNo’]”)).click();
}
}
}
});

 

SQLServer之创建索引视图_小子pk了的博客-CSDN博客_索引视图

mikel阅读(861)

来源: SQLServer之创建索引视图_小子pk了的博客-CSDN博客_索引视图

索引视图创建注意事项

对视图创建的第一个索引必须是唯一聚集索引。 创建唯一聚集索引后,可以创建更多非聚集索引。 为视图创建唯一聚集索引可以提高查询性能,因为视图在数据库中的存储方式与具有聚集索引的表的存储方式相同。 查询优化器可使用索引视图加快执行查询的速度。 要使优化器考虑将该视图作为替换,并不需要在查询中引用该视图。

索引视图中列的 large_value_types_out_of_row 选项的设置继承的是基表中相应列的设置。 此值是使用 sp_tableoption设置的。从表达式组成的列的默认设置为 0。 这意味着大值类型存储在行内。

可以对已分区表创建索引视图,并可以由其自行分区。

若要防止 数据库引擎 使用索引视图,请在查询中包含 OPTION (EXPAND VIEWS) 提示。 此外,任何所列选项设置不正确均会阻止优化器使用视图上的索引。 有关 OPTION (EXPAND VIEWS) 提示的详细信息,请参阅 SELECT (Transact-SQL)。

若删除视图,该视图的所有索引也将被删除。 若删除聚集索引,视图的所有非聚集索引和自动创建的统计信息也将被删除。 视图中用户创建的统计信息受到维护。 非聚集索引可以分别删除。 删除视图的聚集索引将删除存储的结果集,并且优化器将重新像处理标准视图那样处理视图。

可以禁用表和视图的索引。 禁用表的聚集索引时,与该表关联的视图的索引也将被禁用。

索引视图创建要求

创建索引视图需要执行下列步骤并且这些步骤对于成功实现索引视图而言非常重要:

  1. 验证是否视图中将引用的所有现有表的 SET 选项都正确。
  2. 在创建任意表和视图之前,验证会话的 SET 选项设置是否正确。
  3. 验证视图定义是否为确定性的。
  4. 使用 WITH SCHEMABINDING 选项创建视图。
  5. 为视图创建唯一的聚集索引。

索引视图所需的 SET 选项

如果执行查询时启用不同的 SET 选项,则在 数据库引擎 中对同一表达式求值会产生不同结果。 例如,将 SET 选项 CONCAT_NULL_YIELDS_NULL 设置为 ON 后,表达式 ‘ abc ‘ + NULL 会返回值 NULL。 但将 CONCAT_NULL_YIEDS_NULL 设置为 OFF 后,同一表达式会生成 ‘ abc ‘。

为了确保能够正确维护视图并返回一致结果,索引视图需要多个 SET 选项具有固定值。 下表中的 SET 选项必须设置中显示的值为RequiredValue列出现以下情况时:

  • 创建视图和视图上的后续索引。
  • 在创建表时,在视图中引用的基表。
  • 对构成该索引视图的任何表执行了任何插入、更新或删除操作。 此要求包括大容量复制、复制和分布式查询等操作。
  • 查询优化器使用该索引视图生成查询计划。
    SET 选项 必需的值 默认服务器值 ,则“默认”

    OLE DB 和 ODBC 值

    ,则“默认”

    DB-Library 值

    ANSI_NULLS ON ON ON OFF
    ANSI_PADDING ON ON ON OFF
    ANSI_WARNINGS* ON ON ON OFF
    ARITHABORT ON ON OFF OFF
    CONCAT_NULL_YIELDS_NULL ON ON ON OFF
    NUMERIC_ROUNDABORT OFF OFF OFF OFF
    QUOTED_IDENTIFIER ON ON ON OFF

    *将 ANSI_WARNINGS 设置为 ON 隐式将 ARITHABORT 设置为 ON。

    如果使用的是 OLE DB 或 ODBC 服务器连接,则唯一必须要修改的值是 ARITHABORT 设置。 必须使用 sp_configure 在服务器级别或使用 SET 命令从应用程序中正确设置所有 DB-Library 值。极力建议在服务器的任一数据库中创建计算列的第一个索引视图或索引后,尽早在服务器范围内将 ARITHABORT 用户选项设置为 ON。

确定性视图

索引视图的定义必须是确定性的。 如果选择列表中的所有表达式、WHERE 和 GROUP BY 子句都具有确定性,则视图也具有确定性。 在使用特定的输入值集对确定性表达式求值时,它们始终返回相同的结果。 只有确定性函数可以加入确定性表达式。 例如,DATEADD 函数是确定性函数,因为对于其三个参数的任何给定参数值集它总是返回相同的结果。 GETDATE 不是确定性函数,因为总是使用相同的参数调用它,而它在每次执行时返回结果都不同。

要确定视图列是否为确定性列,请使用 COLUMNPROPERTY 函数的 IsDeterministic 属性。 使用 COLUMNPROPERTY 函数的 IsPrecise 属性确定具有架构绑定的视图中的确定性列是否为精确列。 如果为 TRUE,则 COLUMNPROPERTY 返回 1;如果为 FALSE,则返回 0;如果输入无效,则返回 NULL。 这意味着该列不是确定性列,也不是精确列。

即使是确定性表达式,如果其中包含浮点表达式,则准确结果也会取决于处理器体系结构或微代码的版本。 为了确保数据完整性,此类表达式只能作为索引视图的非键列加入。 不包含浮点表达式的确定性表达式称为精确表达式。 只有精确的确定性表达式才能加入键列,并包含在索引视图的 WHERE 或 GROUP BY 子句中。

其他要求

除对 SET 选项和确定性函数的要求外,还必须满足下列要求:

    • 执行 CREATE INDEX 的用户必须是视图所有者。
    • 创建索引时,IGNORE_DUP_KEY 选项必须设置为 OFF(默认设置)。
    • 在视图定义中,表必须由两部分组成的名称(即 schema.tablename**)引用。
    • 必须已使用 WITH SCHEMABINDING 选项创建了在视图中引用的用户定义函数。
    • 视图中引用的任何用户定义函数都必须由两部分组成的名称(即 schema.function**)引用。
    • 用户定义函数的数据访问属性必须为 NO SQL,外部访问属性必须是 NO。
    • 公共语言运行时 (CLR) 功能可以出现在视图的选择列表中,但不能作为聚集索引键定义的一部分。 CLR 函数不能出现在视图的 WHERE 子句中或视图中的 JOIN 运算的 ON 子句中。
    • 在视图定义中使用的 CLR 函数和 CLR 用户定义类型方法必须具有下表所示的属性设置。
      “属性” 注意
      DETERMINISTIC = TRUE 必须显式声明为 Microsoft .NET Framework 方法的属性。
      PRECISE = TRUE 必须显式声明为 .NET Framework 方法的属性。
      DATA ACCESS = NO SQL 通过将 DataAccess 属性设置为 DataAccessKind.None 并将 SystemDataAccess 属性设置为 SystemDataAccessKind.None 来确定。
      EXTERNAL ACCESS = NO 对于 CLR 例程,该属性的默认设置为 NO。
    • 必须使用 WITH SCHEMABINDING 选项创建视图。
    • 视图必须仅引用与视图位于同一数据库中的基表。 视图无法引用其他视图。
    • 视图定义中的 SELECT 语句不能包含下列 Transact-SQL 元素:
      COUNT ROWSET 函数(OPENDATASOURCE、OPENQUERY、OPENROWSET 和 OPENXML) OUTER 联接(LEFT、RIGHT 或 FULL)
      派生表(通过在 FROM 子句中指定 SELECT 语句来定义) 自联接 通过使用 SELECT * 或 SELECT table_name来指定列。*
      DISTINCT STDEV、STDEVP、VAR、VARP 或 AVG 公用表表达式 (CTE)
      float*text, ntext, image, XML,或filestream 子查询 包括排名或聚合开窗函数的 OVER 子句
      全文谓词(CONTAIN、FREETEXT) 引用可为 Null 的表达式的 SUM 函数 ORDER BY
      CLR 用户定义聚合函数 返回页首 CUBE、ROLLUP 或 GROUPING SETS 运算符
      MIN、MAX UNION、EXCEPT 或 INTERSECT 运算符 TABLESAMPLE
      表变量 OUTER APPLY 或 CROSS APPLY PIVOT、UNPIVOT
      稀疏列集 内联或多语句表值函数 OFFSET
      CHECKSUM_AGG

      *索引的视图可以包含float列; 但是,不能在聚集的索引键中包含此类列。

    • 如果存在 GROUP BY,则 VIEW 定义必须包含 COUNT_BIG(*),并且不得包含 HAVING。 这些 GROUP BY 限制仅适用于索引视图定义。 即使一个索引视图不满足这些 GROUP BY 限制,查询也可以在其执行计划中使用该视图。
    • 如果视图定义包含 GROUP BY 子句,则唯一聚集索引的键只能引用 GROUP BY 子句中指定的列。

使用SSMS数据库管理工具创建索引视图

1、连接数据库,选择数据库,展开数据库-》右键视图-》选择新建视图。

2、在添加表弹出框-》选择要创建视图的表、视图、函数、或者同义词等-》点击添加-》添加完成后选择关闭。

3、在关系图窗格中-》选择表与表之间关联的数据列-》选择列的其他排序或筛选条件。

4、右键点击空白处-》选择属性。

5、在视图属性窗格-》绑定到架构选择是-》非重复值选择是。

6、点击保存或者ctrl+s-》查看新创建的视图。

7、在对象资源管理器窗口-》展开视图-》选择视图-》右键点击索引-》选择新建索引-》选择聚集索引。

8、在新建索引弹出框-》选择索引数据列-》索引创建步骤可以参考本博主的创建索引博文-》点击确定(创建唯一聚集索引之后才能创建非聚集索引)。

9、在对象资源管理器中查看视图中的索引。

10、刷新视图-》可以创建非聚集索引,步骤同创建聚集索引(此处省略创建非聚集索引)。

11、点击保存或者ctrl+s-》刷新视图-》查看结果。

12、使用视图。

使用T-SQL脚本创建索引视图

语法:

–声明数据库引用
use 数据库;
go

–判断视图是否存在,如果存在则删除
if exists(select * from sys.views where name=视图名称)
drop view 视图名称;
go

–创建视图
create
view

–视图所属架构的名称。
–[schema_name][.]

–视图名称。 视图名称必须符合有关标识符的规则。 可以选择是否指定视图所有者名称。
[dbo][.]视图名称

–视图中的列使用的名称。 仅在下列情况下需要列名:列是从算术表达式、函数或常量派生的;两个或更多的列可能会具有相同的名称(通常是由于联接的原因);视图中的某个列的指定名称不同于其派生来源列的名称。 还可以在 SELECT 语句中分配列名。
–如果未指定 column,则视图列将获得与 SELECT 语句中的列相同的名称。
–column

with

–适用范围: SQL Server 2008 到 SQL Server 2017 和 Azure SQL Database。
–对 sys.syscomments 表中包含 CREATE VIEW 语句文本的项进行加密。 使用 WITH ENCRYPTION 可防止在 SQL Server 复制过程中发布视图。
–encryption,

–将视图绑定到基础表的架构。 如果指定了 SCHEMABINDING,则不能按照将影响视图定义的方式修改基表或表。 必须首先修改或删除视图定义本身,才能删除将要修改的表的依赖关系。
–使用 SCHEMABINDING 时,select_statement 必须包含所引用的表、视图或用户定义函数的两部分名称 (schema.object)。 所有被引用对象都必须在同一个数据库内。
–不能删除参与了使用 SCHEMABINDING 子句创建的视图的视图或表,除非该视图已被删除或更改而不再具有架构绑定。 否则, 数据库引擎将引发错误。
–另外,如果对参与具有架构绑定的视图的表执行 ALTER TABLE 语句,而这些语句又会影响视图定义,则这些语句将会失败。
schemabinding

–指定为引用视图的查询请求浏览模式的元数据时, SQL Server 实例将向 DB-Library、ODBC 和 OLE DB API 返回有关视图的元数据信息,而不返回基表的元数据信息。
–浏览模式元数据是 SQL Server 实例向这些客户端 API 返回的附加元数据。 如果使用此元数据,客户端 API 将可以实现可更新客户端游标。 浏览模式的元数据包含结果集中的列所属的基表的相关信息。
–对于使用 VIEW_METADATA 创建的视图,浏览模式的元数据在描述结果集内视图中的列时,将返回视图名,而不返回基表名。
–当使用 WITH VIEW_METADATA 创建视图时,如果该视图具有 INSTEAD OF INSERT 或 INSTEAD OF UPDATE 触发器,则视图的所有列(timestamp 列除外)都可更新。 有关可更新视图的详细信息,请参阅“备注”。
–view_metadata

–指定视图要执行的操作。
as
select_statement
go

–创建索引详情请参考索引博客
if not exists (select * from sys.indexes where name=索引名称)
–设置索引
create
unique
clustered
index
索引名称
on
dbo.视图名
(列名 [ asc | desc],列名  [ asc | desc],……);
go

示例:本示例演示在视图上创建一个唯一聚集索引。

–声明数据库引用
use testss;
go

–判断视图是否存在,如果存在则删除
if exists(select * from sys.views where name=’indexview1′)
drop view indexview1;
go

–创建视图
create
view

–视图所属架构的名称。
–[schema_name][.]

–视图名称。 视图名称必须符合有关标识符的规则。 可以选择是否指定视图所有者名称。
dbo.indexview1

–视图中的列使用的名称。 仅在下列情况下需要列名:列是从算术表达式、函数或常量派生的;两个或更多的列可能会具有相同的名称(通常是由于联接的原因);视图中的某个列的指定名称不同于其派生来源列的名称。 还可以在 SELECT 语句中分配列名。
–如果未指定 column,则视图列将获得与 SELECT 语句中的列相同的名称。
–column

with

–适用范围: SQL Server 2008 到 SQL Server 2017 和 Azure SQL Database。
–对 sys.syscomments 表中包含 CREATE VIEW 语句文本的项进行加密。 使用 WITH ENCRYPTION 可防止在 SQL Server 复制过程中发布视图。
–encryption,

–将视图绑定到基础表的架构。 如果指定了 SCHEMABINDING,则不能按照将影响视图定义的方式修改基表或表。 必须首先修改或删除视图定义本身,才能删除将要修改的表的依赖关系。
–使用 SCHEMABINDING 时,select_statement 必须包含所引用的表、视图或用户定义函数的两部分名称 (schema.object)。 所有被引用对象都必须在同一个数据库内。
–不能删除参与了使用 SCHEMABINDING 子句创建的视图的视图或表,除非该视图已被删除或更改而不再具有架构绑定。 否则, 数据库引擎将引发错误。
–另外,如果对参与具有架构绑定的视图的表执行 ALTER TABLE 语句,而这些语句又会影响视图定义,则这些语句将会失败。
schemabinding

–指定为引用视图的查询请求浏览模式的元数据时, SQL Server 实例将向 DB-Library、ODBC 和 OLE DB API 返回有关视图的元数据信息,而不返回基表的元数据信息。
–浏览模式元数据是 SQL Server 实例向这些客户端 API 返回的附加元数据。 如果使用此元数据,客户端 API 将可以实现可更新客户端游标。 浏览模式的元数据包含结果集中的列所属的基表的相关信息。
–对于使用 VIEW_METADATA 创建的视图,浏览模式的元数据在描述结果集内视图中的列时,将返回视图名,而不返回基表名。
–当使用 WITH VIEW_METADATA 创建视图时,如果该视图具有 INSTEAD OF INSERT 或 INSTEAD OF UPDATE 触发器,则视图的所有列(timestamp 列除外)都可更新。 有关可更新视图的详细信息,请参阅“备注”。
–view_metadata

–指定视图要执行的操作。
as
select a.id,a.age,a.height,a.name,b.id as classid from dbo.test1 as a
inner join dbo.test3 as b on a.classid=b.id
–要求对该视图执行的所有数据修改语句都必须符合 select_statement 中所设置的条件。 通过视图修改行时,WITH CHECK OPTION 可确保提交修改后,仍可通过视图看到数据。
–with check option;
go

if not exists (select * from sys.indexes where name=’umiqueindexview1′)
–设置索引
create
unique
clustered
index
umiqueindexview1
on
dbo.indexview1
(name asc);
go

示例结果:因为数据量太小,查询时间和效果不是很明显。

Linux(宝塔)部署.Net Core完整记录 - 果冻栋吖 - 博客园

mikel阅读(1017)

来源: Linux(宝塔)部署.Net Core完整记录 – 果冻栋吖 – 博客园

前言#

最近在V站上看到一个外卖推广的小程序,意思大概是类似淘宝联盟那种,别人走自己的链接后,自己可以抽取大概4%-6%的提成。觉得还蛮有意思的,一开始开源的是静态页面写死的,所以我这边用.Net Core写了个简单的后台。

左边是无后台的,右边红色框是后台配置的。当然功能是很简单的,主要是记录发布到Ubuntu18.4的时候遇到的问题与解决办法。

· · ·

安装宝塔#

宝塔Linux面板是提升运维效率的服务器管理软件,支持一键LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA等100多项服务器管理功能。

这里节省时间直接使用宝塔面板了,这个真的是太方便了,哈哈。安装也非常简单。

因为我使用的是Ubuntu,安装脚本

wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh

其他版本请参考官方文档:https://www.bt.cn/download/linux.html

安装完成后会显示登录地址、用户名、密码信息。登录后浏览器将弹出推荐安装套件,为方便直接一键安装LNMP。

安装.NetCore SDK 3.1#

微软官方文档:https://docs.microsoft.com/zh-cn/dotnet/core/install/linux-ubuntu

因为我使用的18.04,所以找到对应文档。

使用 APT 进行安装可通过几个命令来完成。 安装 .NET 之前,请运行以下命令,将 Microsoft 包签名密钥添加到受信任密钥列表,并添加包存储库。

打开终端并运行以下命令:

Copy
wget https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

安装 SDK#

.NET SDK 使你可以通过 .NET 开发应用。 如果安装 .NET SDK,则无需安装相应的运行时。 若要安装 .NET SDK,请运行以下命令:

Copy
sudo apt-get update; \
  sudo apt-get install -y apt-transport-https && \
  sudo apt-get update && \
  sudo apt-get install -y dotnet-sdk-3.1

安装运行时#

Copy
sudo apt-get update; \
  sudo apt-get install -y apt-transport-https && \
  sudo apt-get update && \
  sudo apt-get install -y aspnetcore-runtime-3.1

作为 ASP.NET Core 运行时的一种替代方法,你可以安装不包含 ASP.NET Core 支持的 .NET 运行时:将上一命令中的 aspnetcore-runtime-5.0 替换为 dotnet-runtime-5.0

Copy
sudo apt-get install -y dotnet-runtime-5.0

其实上述就是照搬微软的官方文档,官方文档还是写的很清楚的。

发布.NetCore项目#

我一开始目标运行时选择的Linux-64,但是出现了这样的错误`错误 NU1605: 检测到包降级: XXXXXXXXXXXXX 从 4.3.0 降级到 XXXXXXXXXXXXX。直接从项目引用包以选择不同版本。

image-20201207114647335

通过查看微软官方文档:https://docs.microsoft.com/zh-cn/nuget/reference/errors-and-warnings/nu1605

问题当在 .NET Core 3.0 或更高版本的项目中同时引用时,与 .NET Core 1.0 和1.1 随附的某些包组合不兼容。 问题包通常以或开头 System. Microsoft. ,并具有4.0.0 和4.3.1 之间的版本号。 在这种情况下,降级消息将具有从运行时开始的包。 依赖关系链。

解决方案若要解决此问题,请添加以下 PackageReference:

Copy
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.0.0" PrivateAssets="all" />

就是添加引用,但实际上你要保证所有项目的包引用版本是一致的。

另一种方法

发布的时候目标运行时直接选择可移植吧~

宝塔面板发布.Net Core项目,并启动项目#

在文件 wwwroot新建项目文件夹,将本地发布文件打包拷贝至服务器解压。

在服务器上终端命令进入部署文件所在目录,然后使用dotnet命令启动服务:

Copy
dotnet XXXXXX.Admin.dll --urls "http://localhost:5000"

image-20201207115503272

Nginx设置反代访问#

现在我们还不能直接访问到我们新部署项目,需要使用Nginx设置反向代理,将特定的端口代理到http://localhost:5000,这一步可以通过宝塔面板来完成,步骤如下:

  • 在宝塔面板上新建一个网站,设置为静态网站即可,并绑定好域名。
  • 在刚才新建的网站中设置反向代理,目标URL填写http://localhost:5000即可,发送域名localhost

image-20201207115748843

image-20201207115850382

浏览器正式可访问项目,此处可能需要重启一下。

使用 Supervisor 守护进程#

现在还有个问题,就是当我们关闭xShell等SSH工具的时候服务进程也会停止运行,我们可使用 Supervisor 守护进程运行。

  • 在宝塔面板上安装Supervisor
  • 添加守护进程(用户建议选择www,不要使用root)

image-20201207120537225

问题#

问题1#

错误 NU1605: 检测到包降级: XXXXXXXXXXXXX 从 4.3.0 降级到 XXXXXXXXXXXXX。直接从项目引用包以选择不同版本

这个问题一开始我按照官方文档修改了,实际还是不可以。所以我选择了可移植发布的。而我在写这篇文章的时候又可以了。

问题2#

验证码我使用了System.Drawing,不过在Linux下的话,这个是无法显示的。

解决办法

System.Drawing.Common 组件提供对GDI+图形功能的访问。它是依赖于GDI+的,那么在Linux上它如何使用GDI+,因为Linux上是没有GDI+的。Mono 团队使用C语言实现了GDI+接口,提供对非Windows系统的GDI+接口访问能力(个人认为是模拟GDI+,与系统图像接口对接),这个就是 libgdiplus。进而可以推测 System.Drawing.Common 这个组件实现时,对于非Windows系统肯定依赖了 ligdiplus 这个组件。如果我们当前系统不存在这个组件,那么自然会报错,找不到它,安装它即可解决。

Ubuntu一键命令

Copy
sudo curl https://raw.githubusercontent.com/stulzq/awesome-dotnetcore-image/master/install/ubuntu.sh|sh

参考:https://www.cnblogs.com/stulzq/p/10172550.html

问题3#

指定端口启动

修改Program.cs

增加代码

Copy
.ConfigureAppConfiguration(builder =>
                {
                    //dotnet test.dll --urls "http://*:5000;https://*:5001"
                    builder.AddCommandLine(args);//设置添加命令行
                })

完整代码

Copy
 public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                //将默认ServiceProviderFactory指定为AutofacServiceProviderFactory https://autofaccn.readthedocs.io/en/latest/integration/aspnetcore.html#asp-net-core-3-0-and-generic-hosting
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureAppConfiguration(builder =>
                {
                    //dotnet test.dll --urls "http://*:5200;https://*:5100"
                    builder.AddCommandLine(args);//设置添加命令行
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

问题4#

验证码生成代码

验证码生成代码应该是蛮多的,我把我的分享下

Copy
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;

namespace XXX.Util
{
   public static class ValidateCodeHelper
   {
       /// <summary>
       /// 验证码的最大长度
       /// </summary>
       public static int MaxLength => 10;

       /// <summary>
       /// 验证码的最小长度
       /// </summary>
       public static int MinLength => 1;

       /// <summary>
       /// 生成验证码
       /// </summary>
       /// <param name="length">指定验证码的长度</param>
       /// <returns></returns>
       public static string CreateValidateCode(int length)
       {
           int[] randMembers = new int[length];
           int[] validateNums = new int[length];
           string validateNumberStr = "";
           //生成起始序列值
           int seekSeek = unchecked((int)DateTime.Now.Ticks);
           Random seekRand = new Random(seekSeek);
           int beginSeek = (int)seekRand.Next(0, Int32.MaxValue - length * 10000);
           int[] seeks = new int[length];
           for (int i = 0; i < length; i++)
           {
               beginSeek += 10000;
               seeks[i] = beginSeek;
           }
           //生成随机数字
           for (int i = 0; i < length; i++)
           {
               Random rand = new Random(seeks[i]);
               int pownum = 1 * (int)Math.Pow(10, length);
               randMembers[i] = rand.Next(pownum, Int32.MaxValue);
           }
           //抽取随机数字
           for (int i = 0; i < length; i++)
           {
               string numStr = randMembers[i].ToString();
               int numLength = numStr.Length;
               Random rand = new Random();
               int numPosition = rand.Next(0, numLength - 1);
               validateNums[i] = Int32.Parse(numStr.Substring(numPosition, 1));
           }
           //生成验证码
           for (int i = 0; i < length; i++)
           {
               validateNumberStr += validateNums[i].ToString();
           }
           return validateNumberStr;
       }
       /// <summary>
       /// 得到验证码图片的长度
       /// </summary>
       /// <param name="validateNumLength">验证码的长度</param>
       /// <returns></returns>
       public static int GetImageWidth(int validateNumLength)
       {
           return (int)(validateNumLength * 12.0);
       }
       /// <summary>
       /// 得到验证码的高度
       /// </summary>
       /// <returns></returns>
       public static double GetImageHeight()
       {
           return 22.5;
       }


       //C# MVC 升级版
       /// <summary>
       /// 创建验证码的图片
       /// </summary> 
       /// <param name="validateCode">验证码</param>
       public static byte[] CreateValidateGraphic(string validateCode)
       {
           Bitmap image = new Bitmap((int)Math.Ceiling(validateCode.Length * 12.0), 22);
           Graphics g = Graphics.FromImage(image);
           try
           {
               //生成随机生成器
               Random random = new Random();
               //清空图片背景色
               g.Clear(Color.White);
               //画图片的干扰线
               for (int i = 0; i < 25; i++)
               {
                   int x1 = random.Next(image.Width);
                   int x2 = random.Next(image.Width);
                   int y1 = random.Next(image.Height);
                   int y2 = random.Next(image.Height);
                   g.DrawLine(new Pen(Color.Silver), x1, y1, x2, y2);
               }
               Font font = new Font("Arial", 12, (FontStyle.Bold | FontStyle.Italic));
               LinearGradientBrush brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height),
                Color.Blue, Color.DarkRed, 1.2f, true);
               g.DrawString(validateCode, font, brush, 3, 2);
               //画图片的前景干扰点
               for (int i = 0; i < 100; i++)
               {
                   int x = random.Next(image.Width);
                   int y = random.Next(image.Height);
                   image.SetPixel(x, y, Color.FromArgb(random.Next()));
               }
               //画图片的边框线
               g.DrawRectangle(new Pen(Color.Silver), 0, 0, image.Width - 1, image.Height - 1);
               //保存图片数据
               MemoryStream stream = new MemoryStream();
               image.Save(stream, ImageFormat.Jpeg);
               //输出图片流
               return stream.ToArray();
           }
           finally
           {
               g.Dispose();
               image.Dispose();
           }
       }
   }
}

总结#

其实就是一篇流水账,记录了发布的过程和遇到的问题及解决办法。之前服务器一直是使用的WinServer,因为熟悉。勇于尝试并去解决问题,慢慢进步~

大学里也学过Linux,受不了。但是真的去使用了,去探索了,嗯,真香~

一个架构师的缓存修炼之路 - IT人的职场进阶 - 博客园

mikel阅读(785)

来源: 一个架构师的缓存修炼之路 – IT人的职场进阶 – 博客园

一位七牛的资深架构师曾经说过这样一句话:

Nginx+业务逻辑层+数据库+缓存层+消息队列,这种模型几乎能适配绝大部分的业务场景。

这么多年过去了,这句话或深或浅地影响了我的技术选择,以至于后来我花了很多时间去重点学习缓存相关的技术。

我在10年前开始使用缓存,从本地缓存、到分布式缓存、再到多级缓存,踩过很多坑。下面我结合自己使用缓存的历程,谈谈我对缓存的认识。

 

01 本地缓存

1. 页面级缓存

我使用缓存的时间很早,2010年左右使用过 OSCache,当时主要用在 JSP 页面中用于实现页面级缓存。伪代码类似这样:

<cache:cache key="foobar" scope="session">   
      some jsp content   
</cache:cache>`

中间的那段 JSP 代码将会以 key=”foobar” 缓存在 session 中,这样其他页面就能共享这段缓存内容。 在使用 JSP 这种远古技术的场景下,通过引入 OSCache 之后 ,页面的加载速度确实提升很快。

但随着前后端分离以及分布式缓存的兴起,服务端的页面级缓存已经很少使用了。但是在前端领域,页面级缓存仍然很流行。

 

2. 对象缓存

2011年左右,开源中国的红薯哥写了很多篇关于缓存的文章。他提到:开源中国每天百万的动态请求,只用 1 台 4 Core 8G 的服务器就扛住了,得益于缓存框架 Ehcache。

这让我非常神往,一个简单的框架竟能将单机性能做到如此这般,让我欲欲跃试。于是,我参考红薯哥的示例代码,在公司的余额提现服务上第一次使用了 Ehcache。

逻辑也很简单,就是将成功或者失败状态的订单缓存起来,这样下次查询的时候,不用再查询支付宝服务了。伪代码类似这样:

添加缓存之后,优化的效果很明显 , 任务耗时从原来的40分钟减少到了5~10分钟。

上面这个示例就是典型的「对象缓存」,它是本地缓存最常见的应用场景。相比页面缓存,它的粒度更细、更灵活,常用来缓存很少变化的数据,比如:全局配置、状态已完结的订单等,用于提升整体的查询速度。

 

3. 刷新策略

2018年,我和我的小伙伴自研了配置中心,为了让客户端以最快的速度读取配置, 本地缓存使用了 Guava,整体架构如下图所示:

那本地缓存是如何更新的呢?有两种机制:

  • 客户端启动定时任务,从配置中心拉取数据。
  • 当配置中心有数据变化时,主动推送给客户端。这里我并没有使用websocket,而是使用了 RocketMQ Remoting 通讯框架。

后来我阅读了 Soul 网关的源码,它的本地缓存更新机制如下图所示,共支持 3 种策略:

▍ zookeeper watch机制

soul-admin 在启动的时候,会将数据全量写入 zookeeper,后续数据发生变更时,会增量更新 zookeeper 的节点。与此同时,soul-web 会监听配置信息的节点,一旦有信息变更时,会更新本地缓存。

▍ websocket 机制

websocket 和 zookeeper 机制有点类似,当网关与 admin 首次建立好 websocket 连接时,admin 会推送一次全量数据,后续如果配置数据发生变更,则将增量数据通过 websocket 主动推送给 soul-web。

▍ http 长轮询机制

http请求到达服务端后,并不是马上响应,而是利用 Servlet 3.0 的异步机制响应数据。当配置发生变化时,服务端会挨个移除队列中的长轮询请求,告知是哪个 Group 的数据发生了变更,网关收到响应后,再次请求该 Group 的配置数据。

不知道大家发现了没?

  • pull 模式必不可少
  • 增量推送大同小异

长轮询是一个有意思的话题 , 这种模式在 RocketMQ 的消费者模型也同样被使用,接近准实时,并且可以减少服务端的压力。

 

02 分布式缓存

关于分布式缓存, memcached 和 Redis 应该是最常用的技术选型。相信程序员朋友都非常熟悉了,我这里分享两个案例。

1. 合理控制对象大小及读取策略

2013年,我服务一家彩票公司,我们的比分直播模块也用到了分布式缓存。当时,遇到了一个 Young GC 频繁的线上问题,通过 jstat 工具排查后,发现新生代每隔两秒就被占满了。

进一步定位分析,原来是某些 key 缓存的 value 太大了,平均在 300K左右,最大的达到了500K。这样在高并发下,就很容易 导致 GC 频繁。

找到了根本原因后,具体怎么改呢? 我当时也没有清晰的思路。 于是,我去同行的网站上研究他们是怎么实现相同功能的,包括: 360彩票,澳客网。我发现了两点:

1、数据格式非常精简,只返回给前端必要的数据,部分数据通过数组的方式返回

2、使用 websocket,进入页面后推送全量数据,数据发生变化推送增量数据

再回到我的问题上,最终是用什么方案解决的呢?当时,我们的比分直播模块缓存格式是 JSON 数组,每个数组元素包含 20 多个键值对, 下面的 JSON 示例我仅仅列了其中 4 个属性。

[{
     "playId":"2399",
     "guestTeamName":"小牛",
     "hostTeamName":"湖人",
     "europe":"123"
 }]

这种数据结构,一般情况下没有什么问题。但是当字段数多达 20 多个,而且每天的比赛场次非常多时,在高并发的请求下其实很容易引发问题。

基于工期以及风险考虑,最终我们采用了比较保守的优化方案:

1)修改新生代大小,从原来的 2G 修改成 4G

2)将缓存数据的格式由 JSON 改成数组,如下所示:

[["2399","小牛","湖人","123"]]

修改完成之后, 缓存的大小从平均 300k 左右降为 80k 左右,YGC 频率下降很明显,同时页面响应也变快了很多。

但过了一会,cpu load 会在瞬间波动得比较高。可见,虽然我们减少了缓存大小,但是读取大对象依然对系统资源是极大的损耗,导致 Full GC 的频率也不低。

3)为了彻底解决这个问题,我们使用了更精细化的缓存读取策略。

我们把缓存拆成两个部分,第一部分是全量数据,第二部分是增量数据(数据量很小)。页面第一次请求拉取全量数据,当比分有变化的时候,通过 websocket 推送增量数据。

第 3 步完成后,页面的访问速度极快,服务器的资源使用也很少,优化的效果非常优异。

经过这次优化,我理解到: 缓存虽然可以提升整体速度,但是在高并发场景下,缓存对象大小依然是需要关注的点,稍不留神就会产生事故。另外我们也需要合理地控制读取策略,最大程度减少 GC 的频率 , 从而提升整体性能。

 

2. 分页列表查询

列表如何缓存是我非常渴望和大家分享的技能点。这个知识点也是我 2012 年从开源中国上学到的,下面我以「查询博客列表」的场景为例。

我们先说第 1 种方案:对分页内容进行整体缓存。这种方案会 按照页码和每页大小组合成一个缓存key,缓存值就是博客信息列表。 假如某一个博客内容发生修改, 我们要重新加载缓存,或者删除整页的缓存。

这种方案,缓存的颗粒度比较大,如果博客更新较为频繁,则缓存很容易失效。下面我介绍下第 2 种方案:仅对博客进行缓存。流程大致如下:

1)先从数据库查询当前页的博客id列表,SQL类似:

select id from blogs limit 0,10 

2)批量从缓存中获取博客id列表对应的缓存数据 ,并记录没有命中的博客id,若没有命中的id列表大于0,再次从数据库中查询一次,并放入缓存,SQL类似:

select id from blogs where id in (noHitId1, noHitId2)

3)将没有缓存的博客对象存入缓存中

4)返回博客对象列表

理论上,要是缓存都预热的情况下,一次简单的数据库查询,一次缓存批量获取,即可返回所有的数据。另外,关于 缓 存批量获取,如何实现?

  • 本地缓存:性能极高,for 循环即可
  • memcached:使用 mget 命令
  • Redis:若缓存对象结构简单,使用 mget 、hmget命令;若结构复杂,可以考虑使用 pipleline,lua脚本模式

第 1 种方案适用于数据极少发生变化的场景,比如排行榜,首页新闻资讯等。

第 2 种方案适用于大部分的分页场景,而且能和其他资源整合在一起。举例:在搜索系统里,我们可以通过筛选条件查询出博客 id 列表,然后通过如上的方式,快速获取博客列表。

 

03 多级缓存

首先要明确为什么要使用多级缓存?

本地缓存速度极快,但是容量有限,而且无法共享内存。分布式缓存容量可扩展,但在高并发场景下,如果所有数据都必须从远程缓存种获取,很容易导致带宽跑满,吞吐量下降。

有句话说得好,缓存离用户越近越高效!

使用多级缓存的好处在于:高并发场景下, 能提升整个系统的吞吐量,减少分布式缓存的压力。

2018年,我服务的一家电商公司需要进行 app 首页接口的性能优化。我花了大概两天的时间完成了整个方案,采取的是两级缓存模式,同时利用了 guava 的惰性加载机制,整体架构如下图所示:

缓存读取流程如下:

1、业务网关刚启动时,本地缓存没有数据,读取 Redis 缓存,如果 Redis 缓存也没数据,则通过 RPC 调用导购服务读取数据,然后再将数据写入本地缓存和 Redis 中;若 Redis 缓存不为空,则将缓存数据写入本地缓存中。

2、由于步骤1已经对本地缓存预热,后续请求直接读取本地缓存,返回给用户端。

3、Guava 配置了 refresh 机制,每隔一段时间会调用自定义 LoadingCache 线程池(5个最大线程,5个核心线程)去导购服务同步数据到本地缓存和 Redis 中。

优化后,性能表现很好,平均耗时在 5ms 左右。最开始我以为出现问题的几率很小,可是有一天晚上,突然发现 app 端首页显示的数据时而相同,时而不同。

也就是说: 虽然 LoadingCache 线程一直在调用接口更新缓存信息,但是各个 服务器本地缓存中的数据并非完成一致。 说明了两个很重要的点:

1、惰性加载仍然可能造成多台机器的数据不一致

2、 LoadingCache 线程池数量配置的不太合理, 导致了线程堆积

最终,我们的解决方案是:

1、惰性加载结合消息机制来更新缓存数据,也就是:当导购服务的配置发生变化时,通知业务网关重新拉取数据,更新缓存。

2、适当调大 LoadigCache 的线程池参数,并在线程池埋点,监控线程池的使用情况,当线程繁忙时能发出告警,然后动态修改线程池参数。

 

写在最后

缓存是非常重要的一个技术手段。如果能从原理到实践,不断深入地去掌握它,这应该是技术人员最享受的事情。

这篇文章属于缓存系列的开篇,更多是把我 10 多年工作中遇到的典型问题娓娓道来,并没有非常深入地去探讨原理性的知识。

我想我更应该和朋友交流的是:如何体系化的学习一门新技术。

  • 选择该技术的经典书籍,理解基础概念
  • 建立该技术的知识脉络
  • 知行合一,在生产环境中实践或者自己造轮子
  • 不断复盘,思考是否有更优的方案

后续我会连载一些缓存相关的内容:包括缓存的高可用机制、codis 的原理等,欢迎大家继续关注。

关于缓存,如果你有自己的心得体会或者想深入了解的内容,欢迎评论区留言。

 

作者简介:985硕士,前亚马逊工程师,现58转转技术总监