双十一电商亿万高并发缓存系统优化方案

电商亿万级高并发系统优化


11.11

11月11日,光棍节,源于这一天日期里有四个阿拉伯数字“1“形似四根光滑的棍子,而光棍在中文有单身的意思,所以光棍节是单身一族的一个另类节日,这个日子便被定为“光棍节”(One’s Day)。

由于天猫品牌的兴起为了品牌推广,天猫团队选择了双十一作为天猫一年一度的大促节日。现如今天猫已然成为全球购物狂欢节。

2017年双十一已过,2017双11狂欢落下帷幕,天猫最终交易额定格在1682亿,创下历史新高。京东全球好物节从11月1日到11月11日24时累计下单金额达1271亿元。

双十一背后的反思

双十一已过,作为程序员的我们是否应该有所思考。在如此庞大的交易额背后是一个什么样的系统在支撑?如果我们是系统开发者,我们应该如何应对如此庞大的交易量?答案就是——缓存!

高并发解决方案

在高并发的场景下,对于读服务来说缓存可以说是抗流量的银弹。而要支撑亿万级流量高峰需要使用多级缓存才能实现。其中包括浏览器缓存、cdn缓存、接入层缓存、应用层缓存、分布式缓存等。

http

浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)

4.1 Expires策略

Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

下面是项目中,浏览器拉取jquery.js web服务器的响应头:

 

注:Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date: Mon,25 Dec 2017 13:39:12 GMT。

Web服务器告诉浏览器在2017-12-25 13:44:12这个时间点之前,可以使用缓存文件。发送请求的时间是2017-12-25 13:39:12,即缓存5分钟。

不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。

4.2 Cache-control策略(重点关注)

Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。

http协议头Cache-Control    
值可以是publicprivateno-cacheno- storeno-transformmust-revalidateproxy-revalidatemax-age

各个消息中的指令含义如下:

1. Public指示响应可被任何缓存区缓存。

2. Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。

3. no-cache指示请求或响应消息不能缓存

4. no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。

5. max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。

6. min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。

7. max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

还是上面那个请求,web服务器返回的Cache-Control头的值为max-age=300,即5分钟(和上面的Expires时间一致,这个不是必须的)。

4.3 页面中使用http缓存

4.3.1 Controller

@Controller

public class PageController {

 

private static Cache<String, Long> lastModifiedCache =

CacheBuilder.newBuilder()

.expireAfterWrite(10, TimeUnit.SECONDS)

.build();

private long getLastModified() throws ExecutionException {

return lastModifiedCache.get(“lastModified”, new Callable<Long>() {

 

@Override

public Long call() throws Exception {

return System.currentTimeMillis();

}

});

}

@RequestMapping(“/index.html”)

public ResponseEntity<String> showPage(@RequestHeader(value=”If-Modified-Since”, required=false)

Date ifModifiedSince) throws Exception {

SimpleDateFormat dateFormat = new SimpleDateFormat(“EEE, d MMM yyy HH:mm:ss ‘GMT'”, Locale.US);

long lastModifiedMillis = getLastModified() / 1000 * 1000;

long now = System.currentTimeMillis() / 1000 * 1000;

long maxAge = 20;

if ( ifModifiedSince != null && ifModifiedSince.getTime() == lastModifiedMillis) {

MultiValueMap<String, String> headers = new HttpHeaders();

headers.add(“Date”, dateFormat.format(new Date(now)));

headers.add(“Expires”, dateFormat.format(new Date(now + maxAge * 1000)));

headers.add(“Cache-Control”, “max-age=” + maxAge);

return new ResponseEntity<>(headers, HttpStatus.NOT_MODIFIED);

}

String body = “<html>”

+ “<head>”

+ “<script type=\”text/javascript\” src=\”/js/jquery-1.6.4.js\” charset=\”utf-8\”></script>”

+ “</head>”

+ “<body>”

+ “<a href=\”#\”>hello</a><p>time:”+System.currentTimeMillis() + “</p>”

+”</body>”

+ “</html>”;

MultiValueMap<String, String> headers = new HttpHeaders();

headers.add(“Date”, dateFormat.format(new Date(now)));

headers.add(“Last-Modified”, dateFormat.format(new Date(lastModifiedMillis)));

headers.add(“Expires”, dateFormat.format(new Date(now + maxAge * 1000)));

headers.add(“Cache-Control”, “max-age=” + maxAge);

return new ResponseEntity<String>(body, headers, HttpStatus.OK);

}

}

4.3.2 页面效果

第一次请求:

 

4.4 静态资源中使用http缓存

SpringMVC对静态资源进行缓存,将js缓存一天时间。

<mvc:resources location=“/js/” mapping=“/js/**” cache-period=86400″/>

 

 

这样配置后,SpringMVC会自动给静态资源Response添加缓存头Cache-Control和Expires值,如下图所示:

 

CDN缓存

5.1 什么是cdn

全称:Content Delivery Network或Content Ddistribute Network,即内容分发网络。

用户向服务器发送请求时,尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

5.2 Cdn处理流程

 

1、当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。

2、CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。

3、用户向CDN的全局负载均衡设备发起内容URL访问请求。

4、CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。

5、区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。

6、全局负载均衡设备把服务器的IP地址返回给用户。

7、用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

5.3 如何获得CDN服务

网络服务提供上一般都提供cdn服务,例如阿里云、腾讯云、百度云等云服务器平台。具体价格请参考运营商报价。

接入层缓存

6.1 Nginx页面缓存

接入层一般使用nginx作为反向代理服务器。Nginx是一款使用异步处理请求的高性能服务器响应速度快。而且nginx的反向代理模块能很好的支持页面缓存和负载均衡。

nginx有proxy_cache这个内置的缓存功能,是基于文件的。如果把缓存路径设置到RAMDISK上面,可以达到和内存缓存差不多的缓存读写速度。这样做虽然解决了文件读写慢的问题,但是如果分布式部署的时候,这个缓存不能跨机器共享。

6.2 Nginx+lua+redis

ngx_lua是Nginx的一个模块,将Lua嵌入到Nginx中,从而可以使用Lua来编写脚本,这样就可以使用Lua编写应用脚本,部署到Nginx中运行,即Nginx变成了一个Web容器;这样开发人员就可以使用Lua语言开发高性能Web应用了。

有了lua脚本就可以解决以下问题:

1、能让nginx主动的检测缓存的过期时间

2、在快过期的时候,直接返回旧的缓存数据

3、使用异步任务更新缓存

使用这个缓存机制之后,用户感知到的平均响应速度提升了10倍。

 

应用缓存

7.1 五分钟法则

1987年,Jim Gray和Gianfranco Putzolu推出了著名的5分钟法则[Gray 1987],他们通过内存,硬盘的性能以及当时的成本,给出了这样的公式:BreakEvenIntervalinSeconds =

(PagesPerMBofRAM /AccessesPerSecondPerDisk) × (PricePerDiskDrive /PricePerMBofRAM)。并由该公式得到了5分钟左右的近似值,因此做出这样的判断,如果一个数据的访问周期在5分钟以内则存放在内存中,否则应该存放在硬盘中。

7.2 缓存回收策略

7.2.1 基于空间

 

即设置缓存的【存储空间】,如设置为10MB,当达到存储空间时,按照一定的策略移除数据。

 

7.2.2 基于容量

 

基于容量指缓存设置了【条目的最大值】,当缓存的条目超过最大大小,则按照一定的策略将旧数据移除。

 

7.2.3 基于时间

TTL(Time To Live ):存活期,即缓存数据从缓存中创建时间开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)。

TTI(Time To Idle):空闲期,即缓存数据多久没被访问过将从缓存中移除的时间。

 

7.2.4 基于Java对象引用

软引用:如果一个对象是软引用,那么当JVM堆内存不足时,垃圾回收器可以回收这些对象。软引用适合用来做缓存,从而当JVM堆内存不足时,可以回收这些对象腾出一些空间供强引用对象使用,从而避免OOM。

 

弱引用:当垃圾回收器回收内存时,如果发现弱引用,则将立即回收它。相对于软引用有更短的生命周期。

 

注意:弱引用/软引用对象只有当没有其他强引用对象引用它时,垃圾回收时才回收该引用。即如果有一个对象(不是弱引用/软引用)引用了弱引用/软引用对象,那么垃圾回收时不会回收该引用对象。

 

7.2.5 回收算法

使用【基于空间】和【基于容量】的会使用一定的策略移除旧数据,常见的如下。

FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除。

LRU(Least Recently Used):   最近最少使用算法,使用时间距离现在最久的那个被移除。

LFU(Least Frequently Used):最不常用算法,一定时间段内使用【次数(频率)】最少的那个被移除。

实际应用中基于LRU的缓存居多,如Guava Cache、Ehcache支持LRU。

 

7.3 Java缓存类型

7.3.1 堆缓存

使用java的堆内存来进行数据的缓存。优点是可以直接将java对象放到缓存中,不需要序列化和反序列化的过程,存储速度非常快。当然缺点也非常明显,当缓存数据量过大时,GC(垃圾回收)暂停时间会变长。而且存储的容量受限于堆内存的大小。一般使用堆内存来存储热点数据。可以使用GuavaCache、Ehcache、MapDB来实现。

7.3.1.1 GuavaCache简介

GuavaCache是google开源java类库Guava的其中一个模块。GuavaCache简单、强大、轻量级,不需要配置文件,使用起来和ConcurrentHashMap一样简单,而且能覆盖绝大多数使用cache的场景需求!

7.3.1.2 创建GuavaCache对象

添加GuavaCache的依赖:

<dependency>

<groupId>com.google.guava</groupId>

<artifactId>guava</artifactId>

<version>19.0</version>

</dependency>

 

GuavaCache是线程安全的,所以可以在系统中是单例的。

final static Cache<String, String> cache = CacheBuilder.newBuilder()

// 设置cache的初始大小为10,要合理设置该值

.initialCapacity(10)

// 设置并发数为5,即同一时间可以有5个线程往cache执行写入操作数值设置越高并发能力越强

.concurrencyLevel(5)

// 设置cache中的数据在写入之后的存活时间为10秒

.expireAfterWrite(10, TimeUnit.SECONDS)

// 缓存这保存的最大key数量

.maximumSize(10000)

// 构建cache实例

.build();

Cache接口定义:

public interface Cache<K, V> {

 

/**

* 可以根据key取缓存的值,如果没有返回null

*

* @since 11.0

*/

@Nullable

V getIfPresent(Object key);

 

/**

* 根据key返回缓存的值,如果没有得到值则调用valueLoader来获得值

* 整个过程为:如果有值则返回,如果没有值则调用valueLoader获得值,然后返回值

* valueLoader要么返回非null值,要么抛出ExecutionException。

* @since 11.0

*/

V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;

 

/**

* 根据keys取缓存中缓存的值,返回一个Map

* @since 11.0

*/

ImmutableMap<K, V> getAllPresent(Iterable<?> keys);

 

/**

* 直接向cache中设置key-value

* @since 11.0

*/

void put(K key, V value);

 

/**

* 将一个map中的内容添加到缓存

* @since 12.0

*/

void putAll(Map<? extends K,? extends V> m);

 

/**

* 删除key对应的缓存数据

*/

void invalidate(Object key);

 

/**

* 删除多个key对于的缓存数据

*

* @since 11.0

*/

void invalidateAll(Iterable<?> keys);

 

/**

* 删除缓存中的全部数据,即清空缓存

*/

void invalidateAll();

 

/**

* 当前缓存中的key的数量

*/

long size();

 

/**

* 返回当前缓存统计信息的快照,并且将所有统计数据重置为0

*/

CacheStats stats();

 

/**

* 将当前缓存的中的数据全部返回,返回一个ConcurrentMap对象

*/

ConcurrentMap<K, V> asMap();

 

/**

* 执行一些维护操作,包括清理缓存

*/

void cleanUp();

}

 

 

7.3.1.3 使用GuavaCache方法一

public class GuavaCacheTest {

 

final static Cache<String, String> cache = CacheBuilder.newBuilder()

// 设置cache的初始大小为10,要合理设置该值

.initialCapacity(10)

// 设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作

.concurrencyLevel(5)

// 设置cache中的数据在写入之后的存活时间为10秒

.expireAfterWrite(10, TimeUnit.SECONDS)

// 缓存这保存的最大key数量

.maximumSize(10000)

// 构建cache实例

.build();

public String sayHello(String name) {

//从cache中取值

String result = cache.getIfPresent(name);

//如果有值直接返回

if (result != null && !””.equals(result)) {

return result;

}

//如果没有值在查询数据

result = getResult(name);

//把数据添加到缓存

cache.put(name, result);

//返回结果

return result;

}

private String getResult(String key) {

System.out.println(“getResult is executed!”);

return “hello:” + key;

}

@Test

public void testSayHello() {

for (int i=0; i < 20;i++) {

String result = sayHello(“小米”);

System.out.println(result);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

 

7.3.1.4 使用GuavaCache方法二

public class GuavaCacheTest2 {

 

final static Cache<String, String> cache = CacheBuilder.newBuilder()

// 设置cache的初始大小为10,要合理设置该值

.initialCapacity(10)

// 设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作

.concurrencyLevel(5)

// 设置cache中的数据在写入之后的存活时间为10秒

.expireAfterWrite(10, TimeUnit.SECONDS)

// 缓存这保存的最大key数量

.maximumSize(10000)

// 构建cache实例

.build();

public String sayHello(final String name) {

//从cache中取值

String result = “”;

try {

result = cache.get(name, new Callable<String>() {

 

@Override

public String call() throws Exception {

String res = getResult(name);

return res;

}

});

} catch (ExecutionException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

//返回结果

return result;

}

private String getResult(String key) {

System.out.println(“getResult is executed!”);

return “hello:” + key;

}

@Test

public void testSayHello() {

for (int i=0; i < 20;i++) {

String result = sayHello(“小米”);

System.out.println(result);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

 

 

 

7.3.2 堆外缓存

把缓存的数据放到堆外内存,不受java虚拟机内存限制,可以有很大的存储空间,当然受制于服务器的物理内存。不影响GC的速度。但是缓存的数据需要进行序列化和反序列化的过程,存取速度低于堆内缓存。

可以使用EhCache、MapDB来实现。

7.3.2.1 MapDB简介

MapDB是一个开放源代码(Apache 2.0授权),嵌入式Java数据库引擎和收集框架。它提供带有范围查询、时效限制、压缩、超栈存储和流功能的map、set、list、queue、Bitmap。MapDB可能是当前最快的Java数据库,性能可与java.util 集合相当。它还提供高级功能,如ACID事务,快照,增量备份等等。

7.3.2.2 MapDB堆外缓存

向工程中添加堆MapDB的jar包

<dependency>

<groupId>org.mapdb</groupId>

<artifactId>mapdb</artifactId>

<version>3.0.5</version>

</dependency>

 

MapDB同样支撑堆内缓存,其底层依然是GuavaCache,所以MapDB堆内缓存的使用方法在此就不再介绍,此处主要是看MapDB堆外缓存的使用方法。

public class MapDBTest {

private static DB db;

private static HTreeMap<String , String> userMap;

static {

db = DBMaker

//堆内内存保存数据,不对数据进行序列化,速度最快

//.heapDB()

//同样是使用堆内存保存数据,会把数据序列化成byte[],保存的数据不受GC影响

//.memoryDB()

//堆外内存,不受GC影响,不受JVM内存影响,不受GC影响

//可以使用JVM的参数 -XX:MaxDirectMemorySize=10G 设置堆外内存的大小

.memoryDirectDB()

.make();

userMap = db.hashMap(“userMap”, Serializer.STRING, Serializer.STRING)

// create() 创建新的集合。 如果集合存在,将扔出异常。

// open() 打开存在的集合。 如果集合不存在,将扔出异常。

// createOrOpen() 如果存在就打开, 否则创建。

.createOrOpen();

}

/**

* 堆外内存测试

*/

@Test

public void testOffHeap() throws Exception {

new Thread() {

 

@Override

public void run() {

super.run();

while(true) {

System.out.println(“userMap size:” + userMap.size());

KeySet<String> keys = userMap.getKeys();

for (Object key : keys) {

System.out.print(“key:” + key);

System.out.print(“\tvlaue:” + userMap.get(key));

}

System.out.println();

try {

Thread.sleep(500);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}.start();

userMap.put(“hello”, “world!”);

Thread.sleep(1000);

userMap.put(“hello1”, “100000”);

Thread.sleep(1000);

userMap.put(“hello2”, “abcdefg”);

Thread.sleep(2000);

}

}

 

7.3.3 磁盘缓存

堆缓存和对外缓存都有一个问题,就是当系统重启后缓存的数据就消失了。如果想重启后仍然保留缓存的数据,可以使用磁盘缓存,这样当系统重启后会自动加载缓存的数据,相当于把数据持久化了。

可以使用Ehcache、MapDB来实现。

7.3.3.1 MapDB实现磁盘缓存

public class MapDBTest2 {

private static DB db;

private static HTreeMap<String , String> userMap;

static {

db = DBMaker

//文件缓存,将缓存数据保存到文件中

.fileDB(“D:\\temp\\mapdb\\user.db”)

//在java程序结束之前先关闭db,保证数据文件的完整性

.closeOnJvmShutdown()

.transactionEnable()

.make();

userMap = db.hashMap(“userMap”, Serializer.STRING, Serializer.STRING)

// create() 创建新的集合。 如果集合存在,将扔出异常。

// open() 打开存在的集合。 如果集合不存在,将扔出异常。

// createOrOpen() 如果存在就打开, 否则创建。

.createOrOpen();

}

/**

* 磁盘缓存测试

*/

@Test

public void testFileCache() throws Exception {

new Thread() {

 

@Override

public void run() {

super.run();

while(true) {

System.out.println(“userMap size:” + userMap.size());

KeySet<String> keys = userMap.getKeys();

for (Object key : keys) {

System.out.print(“key:” + key);

System.out.print(“\tvlaue:” + userMap.get(key));

}

System.out.println();

try {

Thread.sleep(500);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}.start();

/*userMap.put(“hello”, “world!”);

Thread.sleep(1000);

userMap.put(“hello1”, “100000”);

Thread.sleep(1000);

userMap.put(“hello2”, “abcdefg“);

db.commit();*/

Thread.sleep(2000);

}

}

 

7.3.4 小结

在java中使用缓存,一般都是使用多级缓存,入下图所示:

做缓存时,最热的数据放到堆缓存中,较热的数据放到堆外缓存中,不太热的数据放到磁盘缓存中。如果硬要缓存不能命中的话就到分布式缓存中查询。

 

分布式缓存

在前面接入层缓存及应用层缓存的使用过程中都使用到了分布式缓存。目前最流行的分布式缓存工具就是redis。当然redis是一个key-value形式的nosql数据库,非常适合于做缓存,因为其可以将缓存数据持久化,重启服务后缓存数据还可以保留,可以有效的防止缓存雪崩。

8.1 使用redis做缓存

Redis中有五种数据类型,分别为:

String

Hash

List

Set

SortedSet

其中适合做缓存的数据类型有string和hash两种。

在redis中String是一个key对应一个字符串,并且可以设置key的过期时间,在做缓存是此功能非常实用。

Hash数据类型相当于是一个key对应一个map,在map中海油key-value形式的存储。也是可以做缓存的,因为最终还是key-value形式,但是hash中的小key是不可以设置过期时间的,一旦设置就会永久保存,如果需要数据同步时需要手动完成。

8.1.1 Java客户端Jedis

添加jar包:

<dependency>

<groupId>redis.clients</groupId>

<artifactId>jedis</artifactId>

<version>${jedis.version}</version>

</dependency>

 

使用步骤:

  • 创建一个Jedis对象,需要host、port参数
  • 直接使用jedis操作redis,每个redis命令对应一个方法。
  • 打印结果
  • 关闭Jedis对象
@Test

public void testJedis() throws Exception {

// 1、创建一个Jedis对象,需要host、port参数

Jedis jedis = new Jedis(“192.168.25.153”, 6379);

// 2、直接使用jedis操作redis,每个redis命令对应一个方法。

String result = jedis.get(“hello”);

System.out.println(result);

String value1 = jedis.hget(“hash1”, “key1”);

System.out.println(value1);

// 3、打印结果

// 4、关闭Jedis对象

jedis.close();

}

8.1.2 JedisPool的使用方法

使用方法:

  • 创建一个JedisPool对象,构造参数host、port。JedisPool是单例存在
  • 从JedisPool对象获得一个连接Jedis对象。多例的
  • 使用Jedis管理redis
  • 使用完毕之后关闭jedis对象。让连接池回收连接。
  • 系统结束之前关闭JedisPool。
@Test

public void testJedisPool() throws Exception {

// 1、创建一个JedisPool对象,构造参数host、port。JedisPool是单例存在

JedisPool jedisPool = new JedisPool(“192.168.25.153”, 6379);

// 2、从JedisPool对象获得一个连接Jedis对象。多例的

Jedis jedis = jedisPool.getResource();

// 3、使用Jedis管理redis

String string = jedis.get(“hello”);

System.out.println(string);

// 4、使用完毕之后关闭jedis对象。让连接池回收连接。

jedis.close();

// 5、系统结束之前关闭JedisPool。

jedisPool.close();

}

 

8.2 RedisCluster

Redis之所以速度快,就是因为所有的数据都是保存在内存中。所有存储的数据量受限于物理内存的大小,如果要存储的数据量非常大就需要使用redis集群,即redisCluster。

8.2.1 redis-cluster架构图

 

架构细节:

(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.

(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.

(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点

8.2.2 redis-cluster投票:容错

 

(1)领着投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉.

(2):什么时候整个集群不可用(cluster_state:fail)?

如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完成时进入fail状态.

ps:当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误

 

8.2.3 使用JedisCluster管理redis集群

需要使用JedisCluster类管理集群。

使用方法:

  • 创建一个JedisCluster对象,构造参数需要一个Set类型。Set中有多个HostAndPort对象。
  • JedisCluster在系统中是单例存在
  • 如果对redis集群进行操作的话直接使用JedisCluster对象管理集群。
  • 使用完毕之后,不许要关闭JedisCluster。系统结束之前关闭即可。

 

 

如何缓存数据

9.1 过期缓存与不过期缓存

缓存有过期和不过期之分,需要根据实际的业务情况而定。如果缓存的数据是热点数据,例如首页上的内容,每次展示首页都需要加载展示,那么这时候就可以设置成不过期的缓存。

不过期缓存的设置方式:

  • 开启事务
  • 执行sql语句
  • 提交事务
  • 写缓存

需要注意的是,不要在事务中写缓存,尤其是写分布式缓存,因为网络抖动可能导致写缓存响应时间很慢,引起数据库事务阻塞。

如果缓存空间有限,并且是低频热点缓存数据,可以考虑给缓存设置过期时间。设置方式:

  • 先到缓存中查询数据
  • 如果查到数据则返回结果
  • 如果没有查到数据则查询数据库
  • 把查询结果添加到缓存。
  • 设置缓存的过期时间。

这种方式可能会出现短时间内数据不一致的情况,这时候需要根据业务情况设置缓存的过期时间。如果实在不能忍受也可以使用代码直接更新缓存。

9.2 维度化缓存

在电商系统中,一个商品可以拆分成商品基本信息,商品图片、上下架、规格参数、商品介绍等。如果商品发生变化则需要将所有的商品信息都更新一遍,这样的话更新成本会很高,包括接口调用量和带宽。所以最好将数据进行维度化并增量更新,也就是说将商品信息划分成多个维度的缓存数据,如果某些内容更新后,只更新相关的一个维度即可。例如商品的上下架操作,只更新上下架的状态即可,不需要将商品的全部信息。

9.3 大Value缓存

在做缓存时,如果缓存的value数据量特别大时需要格外注意,尤其是在使用redis时。因为redis是单线程处理的,因为某个大value的缓存的存储占用了线程资源,会导致这个redis的系统阻塞,所有的命令都需要等待。所以当遇到这种大value缓存时,可以考虑将这个大value拆分成若干个小value,取值时可以在工程中进行组合。

图片[1]-双十一电商亿万高并发缓存系统优化方案-小蜜蜂资源网

图片[2]-双十一电商亿万高并发缓存系统优化方案-小蜜蜂资源网

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容