大家好,我是IT修真院郑州分院第十期学员,一枚正直纯洁善良的JAVA程序员。
今天给大家分享一下,修真院官网JAVA任务六,扩展思考中的知识点——为什么要使用MEMCACHE?MEMCACHE有什么作用?
一、背景介绍
memcache是什么?
memcache是一个高性能的分布式的内存对象缓存系统,用于动态Web应用以减轻数据库负担。它通过在内存中缓存数据和对象,来减少读取数据库的次数。从而提高动态、数据库驱动网站速度。
memcache通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。memcache主要用于分担数据库负的压力,memcache将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。
二、知识剖析
1.memcache的工作原理:
首先memcached是以守护程序方式运行于一个或多个服务器中,随时接受客户端的连接操作。
客户端在与memcached服务建立连接之后,接下来的事情就是存取对象了,每个被存取的对象都有一个唯一的标识符key,存取操作均通过这个key进行,保存到memcached中的对象实际上是放置内存中的,并不是保存cache文件中的,这也是为什么memcached能够如此高效快速的原因。注意,这些对象并不是持久的,服务停止之后,里边的数据就会丢失。
>memcache采用了C/S的模式,在server端启动服务进程,在启动时可以指定监听的ip、自己的端口号,所使用的内存大小等几个关键参数。一旦启动,服务就一直处于可用状态。
2.memcache的简单使用(cmd命令行),操作详见代码实战;
(1)set key flags exptime bytes [noreply] value(第二行);
(2)add key flags exptime bytes [noreply] value(第二行);
(3)replace key flags exptime bytes [noreply] value(第二行);
(4)append key flags exptime bytes [noreply] value(第二行);
(5)preppend key flags exptime bytes [noreply] value(第二行);
(6)get key(单个) get key1 key2 key3(多个,空格隔开);
(7)delete key [noreply]。
各参数含义:
key:键值 key-value 结构中的 key,用于查找缓存值。
flags:可以包括键值对的整型参数,客户机使用它存储关于键值对的额外信息 。
exptime:在缓存中保存键值对的时间长度(以秒为单位,0 表示永远).
bytes:在缓存中存储的字节数.
noreply(可选): 该参数告知服务器不需要返回数据。
value:存储的值(始终位于第二行)(可直接理解为key-value结构中的value)。
3.在springMVC项目中使用memcache(详见代码实战)
三、常见问题
1.为什么会有memcache和memcached两种名称?
memcache是这个项目的名称,而memcached是它服务器端的主程序文件名。
2.memcache内置内存存储方式:
为了提高性能,memcached中保存的数据都存储在memcache内置的内存存储空间中。由于数据仅存在于内存中,因此,重启memcached、重启操作系统就会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。
3.Memcached的缓存策略:
Memcached的缓存策略是LRU(最近最少使用)加上到期失效策略。当你在memcached内存储数据项时,你有可能会指定它在缓存的失效时间,默认为永久。当memcached服务器用完分配的内时,失效的数据被首先替换,然后也是最近未使用的数据。在LRU中,memcached使用的是一种Lazy Expiration策略,自己不会监控存入的key/vlue对是否过期,而是在获取key值时查看记录的时间戳,检查key/value对空间是否过期,这样可减轻服务器的负载。
四、编码实战
1.memcache的简单使用(cmd命令行);
(1)链接已经启动的memcache。注意一定要启动,连接时要与启动时的端口号一致。
(2)操作“增删改查”;
添加并查看:
添加、查看、替代、查看是否成功;
添加、查看、后面追加、前面追加以及查看;
添加、查看、删除、查看是否删除成功;
2.在springMVC项目中使用memcache缓存。
(1)导入jar包;
<!-- https://mvnrepository.com/artifact/com.whalin/Memcached-Java-Client -->
<dependency>
<groupId>com.whalin</groupId>
<artifactId>Memcached-Java-Client</artifactId>
<version>3.0.2</version>
</dependency>
(2)memcache.properties;
#######################设置Memcached服务器参数#######################
#设置服务器地址
memcached.server=127.0.0.1
#该端口号
memcached.port=11210
#容错
memcached.failOver=true
#设置初始连接数
memcached.initConn=20
#设置最小连接数
memcached.minConn=10
#设置最大连接数
memcached.maxConn=50
#设置连接池维护线程的睡眠时间
memcached.maintSleep=3000
#设置是否使用Nagle算法(Socket的参数),如果是true在写数据时不缓冲,立即发送出去
memcached.nagle=false
#设置socket的读取等待超时时间
memcached.socketTO=3000
#设置连接心跳监测开关
memcached.aliveCheck=true
#######################设置Memcached服务器参数#######################
(3)在applicationContext.xml中引入;
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="locations">
<list>
<value>classpath:memcache.properties</value>
</list>
</property>
</bean>
<!-- Memcached配置 -->
<bean id="memcachedPool" class="com.whalin.MemCached.SockIOPool"
factory-method="getInstance" init-method="initialize" destroy-method="shutDown">
<constructor-arg>
<value>memcache</value>
</constructor-arg>
<property name="servers">
<list>
<value>${memcached.server}:${memcached.port}</value>
</list>
</property>
<property name="initConn">
<value>${memcached.initConn}</value>
</property>
<property name="minConn">
<value>${memcached.minConn}</value>
</property>
<property name="maxConn">
<value>${memcached.maxConn}</value>
</property>
<property name="maintSleep">
<value>${memcached.maintSleep}</value>
</property>
<property name="nagle">
<value>${memcached.nagle}</value>
</property>
<property name="socketTO">
<value>${memcached.socketTO}</value>
</property>
</bean>
(4)MemcacheUtils工具类;
package com.zyq.util;
import com.whalin.MemCached.MemCachedClient;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* @创建人 zyq
* @创建时间${date}
* @描述
*/
public class MemcacheUtils {
private static Logger logger = LogManager.getLogger(MemcacheUtils.class);
/**
* cachedClient.
*/
private static MemCachedClient cachedClient;
static {
if (cachedClient == null) {
cachedClient = new MemCachedClient("memcache");
}
}
/**
* 构造函数.
*/
public MemcacheUtils() {
}
/**
* 向缓存添加新的键值对。如果键已经存在,则之前的值将被替换.
*
* @param key 键
* @param value 值
* @return boolean
*/
public static boolean set(String key, Object value) {
return setExp(key, value, null);
}
/**
* 向缓存添加新的键值对。如果键已经存在,则之前的值将被替换.
*
* @param key 键
* @param value 值
* @param expire 过期时间 NewDate(1000*10):十秒后过期
* @return boolean
*/
public static boolean set(String key, Object value, Date expire) {
return setExp(key, value, expire);
}
/**
* 向缓存添加新的键值对。如果键已经存在,则之前的值将被替换.
*
* @param key 键
* @param value 值
* @param expire 过期时间 New Date(1000*10):十秒后过期
* @return boolean
*/
private static boolean setExp(String key, Object value, Date expire) {
boolean flag = false;
try {
flag = cachedClient.set(key, value, expire);
} catch (Exception e) {
logger.error("Memcached set方法报错,key值:" + key + "\r\n" + exceptionWrite(e));
}
return flag;
}
/**
* 仅当缓存中不存在键时,add 命令才会向缓存中添加一个键值对.
*
* @param key 键
* @param value 值
* @return boolean
*/
public static boolean add(String key, Object value) {
return addExp(key, value, null);
}
/**
* 仅当缓存中不存在键时,add 命令才会向缓存中添加一个键值对.
*
* @param key 键
* @param value 值
* @param expire 过期时间 NewDate(1000*10):十秒后过期
* @return boolean
*/
public static boolean add(String key, Object value, Date expire) {
return addExp(key, value, expire);
}
/**
* 仅当缓存中不存在键时,add 命令才会向缓存中添加一个键值对.
*
* @param key 键
* @param value 值
* @param expire 过期时间 NewDate(1000*10):十秒后过期
* @return boolean
*/
private static boolean addExp(String key, Object value, Date expire) {
boolean flag = false;
try {
flag = cachedClient.add(key, value, expire);
} catch (Exception e) {
logger.error("Memcached add方法报错,key值:" + key + "\r\n" + exceptionWrite(e));
}
return flag;
}
/**
* 仅当键已经存在时,replace命令才会替换缓存中的键.
*
* @param key 键
* @param value 值
* @return boolean
*/
public static boolean replace(String key, Object value) {
return replaceExp(key, value, null);
}
/**
* 仅当键已经存在时,replace命令才会替换缓存中的键.
*
* @param key 键
* @param value 值
* @param expire 过期时间 NewDate(1000*10):十秒后过期
* @return boolean
*/
public static boolean replace(String key, Object value, Date expire) {
return replaceExp(key, value, expire);
}
/**
* 仅当键已经存在时,replace命令才会替换缓存中的键.
*
* @param key 键
* @param value 值
* @param expire 过期时间 NewDate(1000*10):十秒后过期
* @return boolean
*/
private static boolean replaceExp(String key, Object value, Date expire) {
boolean flag = false;
try {
flag = cachedClient.replace(key, value, expire);
} catch (Exception e) {
logger.error("Memcachedreplace方法报错,key值:" + key + "\r\n" + exceptionWrite(e));
}
return flag;
}
/**
* get 命令用于检索与之前添加的键值对相关的值.
*
* @param key 键
* @return boolean
*/
public static Object get(String key) {
Object obj = null;
try {
obj = cachedClient.get(key);
} catch (Exception e) {
logger.error("Memcached get方法报错,key值:" + key + "\r\n" + exceptionWrite(e));
}
return obj;
}
/**
* 删除 memcached 中的任何现有值.
*
* @param key 键
* @return boolean
*/
public static boolean delete(String key) {
return cachedClient.delete(key);
}
/**
* 清理缓存中的所有键/值对.
*
* @return boolean
*/
public static boolean flashAll() {
boolean flag = false;
try {
flag = cachedClient.flushAll();
} catch (Exception e) {
logger.error("MemcachedflashAll方法报错\r\n" + exceptionWrite(e));
}
return flag;
}
/**
* 返回异常栈信息,String类型.
*
* @param e Exception
* @return boolean
*/
private static String exceptionWrite(Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.flush();
return sw.toString();
}
}
(5)学生列表页controller(调用工具类的方法判断);
// 查询所有学生信息,上下页跳转使用
@RequestMapping(value = "/studentS",method = RequestMethod.GET)
public ModelAndView selectAllByPage(Integer currPage){
logger.info("输入参数:当前页为"+currPage+"。");
ModelAndView modelAndView = new ModelAndView("myView");
Page<Student> page;
if (MemcacheUtils.get(currPage.toString())==null){
logger.info(currPage.toString()+"这一页没有缓存,已经添加缓存");
page = studentService.selectAllByPage(currPage);
MemcacheUtils.set(currPage.toString(),page);
}else {
page = (Page<Student>) MemcacheUtils.get(currPage.toString());
logger.info(currPage.toString()+"这一页有缓存,不用添加缓存");
}
modelAndView.addObject("page",page);
modelAndView.addObject("item","viewsBody");
logger.info("***********************分割线***************************");
return modelAndView;
}
(6)运行项目,查看效果(进入页面,然后刷新);
(7)也可以利用命令行查看,可以看到get 1有对应的值,但是因为存入的是实体类,所以会乱码;
五、扩展思考
1. 缓存穿透及解决?
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决1:采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
解决2:访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。
2.缓存雪崩及解决
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决:这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
3.缓存击穿及解决
一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。
解决:在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。
六、参考文献
https://www.cnblogs.com/mxmbk/articles/6586229.html
https://www.cnblogs.com/lixuwu/p/7446170.html
https://blog.csdn.net/fei33423/article/details/79027790
七、更多讨论
1.memcache和redis区别
Memcache:
Memcache可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS,适用于最大程度扛量
只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失
Redis:
支持多种数据结构,如string,list,dict,set,zset,hyperloglog
单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。
支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。
支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制.
支持pub/sub消息订阅机制,可以用来进行消息订阅与通知。
支持简单的事务需求,但业界使用场景很少,并不成熟
2.memcached命令add和set的区别
set 命令用于向缓存添加新的键值对。如果键已经存在,则之前的值将被替换。
使用add添加新的键值对,仅当缓存中不存在键时,add 命令才会向缓存中添加一个键值对。如果缓存中已经存在键,则之前的值将仍然保持不变,并且将获得响应 NOT_STORED。
3.什么样的数据适合写入缓存
访问频率高、修改频率低、数据量小的数据