发表于: 2020-07-18 23:09:20
1 1443
今天完成的事情:小课堂内容补充
明天计划的事情:小课堂分享
遇到的问题:
收获:
Memcached
1.背景介绍
假设又这样一个场景,前台有一个操作,传过来一个请求到后台,这时候需要操作数据库,一顿操作下来查询数据大概耗时300ms,然后后面长时间内数据不会变化,然后这样的请亲又来了一次,还是耗时300ms,如果我们使用缓存会怎样,把刚才查询的结果按照key-value的形式扔进缓存,通过key查询出来耗时只要2ms,后面的所有一样的请求查询就只用2ms。同时也因为处理速度快,能够支持高并发。
缓存是高性能下提高热点数据访问性能的有效手段。用于处理系统的高并发,高性能。
综上,像Memcache,Redis这样的缓存组件就诞生了。
2.知识剖析
缓存
缓存分类:本地缓存,分布式缓存,多级缓存
本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
分布式缓存可以很好得解决这个问题。分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。
淘汰机制:不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。一般的剔除策略有 FIFO 淘汰最早数据、LRU 剔除最近最少使用、和 LFU 剔除最近使用频率最低的数据几种策略。
Memcache
后面我会把 Memcache 简称为 MC。
MC是一个自由开源的,高性能,分布式内存对象缓存系统,是一种基于内存的key-value存储,用来存储小块的任意数据(字符串、对象)。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
安装:
(1)首先需要安装Libevent,Libevent是一款跨平台的事件处理接口的封装,可以兼容多个操作系统的事件访问。 Memcached的安装依赖于Libevent,因此需要先完成Libevent的安装。
yum install gcc gcc-c++ make -y #yum安装gcc编译环境包
(2)解压软件包
tar -zxvf libevent-2.1.8-stable.tar.gz
tar -zxvf memcached-1.5.6.tar.gz
cd libevent-2.1.8-stable/
./configure --prefix=/software/local/libevent
make && make install
到此libevent安装完毕
(3).安装Memcached
cd ../memcached-1.5.6/
./configure \
--prefix=/software/local/memcached \
--with-libevent=/software/local/libevent/ #指定libevent安装路径
make && make install
(4).创建软连接,方便memcached服务
ln -s /software/local/memcached/bin/* /usr/local/bin/
红色的是我自己安装的路径,自己上传文件在那个路径下,就在哪个路径下操作,一般都是在/usr/local 下
(5).启动服务`
启动 memcached(-d:守护进程、-m:指定缓存大小为32M 、-p:指定默认端口11211 、 -u:指定 登陆用户为 root)
memcached -d -m 32m -p 11211 -u root
netstat -antp | grep memcached #查看启动监听端口
6.连接memcached数据库
telnet HOST PORT
命令中的 HOST 和 PORT 为运行 Memcached 服务的 IP 和 端口。没有telnet指令的需要自己安装。
特点:
1.MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
2.MC 功能简单,使用内存存储数据,只支持 K-V 结构,不提供持久化和主从同步功能;
3.MC 的内存结构以及钙化问题后面会详细介绍;
4.MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
5.失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
6.当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。
另外,使用 MC 有一些限制:
key 不能超过 250 个字节;
value 不能超过 1M 字节;
key 的最大失效时间是 30 天;
内存结构:
首先来看 MC 的内存结构。MC 默认是通过 Slab Allocator 来管理内存,如下图所示。Slab 机制主要是用来解决频繁 malloc/free 会产生内存碎片的问题。
如图,MC 会把内存分为许多不同类型的 Slab,每种类型 Slab 用来保存不同大小的对象。每个 Slab 由若干的 Page 组成,如图中浅绿色的模块。不同 Slab 的 Page,默认大小是一样的,都是 1M,这也是默认 MC 存储对象不能超过 1M 的原因。每个 Page 内又划分为许多的 Chunk,Chunk 就是实际用来保存对象的空间,就是图中橘色的。不同类型的 Slab 中 Chunk 的大小是不同的,当保存一个对象时,MC 会根据对象的大小来选择最合适的 Chunk 来存储,减少空间浪费。
Slab Allocator 创建 Slab 时的参数有三个,分别是 Chunk 大小的增长因子,Chunk 大小的初始值以及 Page 的大小。在运行时会根据要保存的对象大小来逐渐创建 Slab。
钙化问题:
考虑这样一个场景,使用 MC 来保存用户信息,假设单个对象大约 300 字节。这时会产生大量的 384 字节大小的 Slab。运行一段时间后,用户信息增加了一个属性,单个对象的大小变成了 500 字节,这时再保存对象需要使用 768 字节的 Slab,而 MC 中的容量大部分创建了 384 字节的 Slab,所以 768 的 Slab 非常少。这时虽然 384 Slab 的内存大量空闲,但 768 Slab 还是会根据 LRU 算法频繁剔除缓存,导致 MC 的剔除率升高,命中率降低。这就是所谓的 MC 钙化问题。
解决钙化问题可以开启 MC 的 Automove 机制,每 10s 调整 Slab。也可以分批重启 MC 缓存,不过要注意重启时要进行一定时间的预热,防止雪崩问题。另外,在使用 Memcached 时,最好计算一下数据的预期平均长度,调整 growth factor, 以获得最恰当的设置,避免内存的大量浪费。
3.常见问题/解决方案
对使用缓存时常遇到几个问题,整理出一个表格,如下图所示。
缓存更新方式
第一个问题是缓存更新方式,这是决定在使用缓存时就该考虑的问题。
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。
当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。
这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。
但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。
数据不一致
第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。
解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。
缓存穿透
第三个问题是缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。
解决的办法如下。
对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。
缓存击穿
第四个问题是缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。
解决这个问题有如下办法。
可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
缓存雪崩
第五个问题是缓存雪崩。产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
解决方法:
使用快速失败的熔断策略,减少 DB 瞬间压力;
使用主从模式和集群模式来尽量保证缓存服务的高可用。
实际场景中,这两种方法会结合使用。
5.编码实战
java中运用
(1). 导入jar包。
java_memcached-release_2.6.6.jar
commons-pool-1.5.6.jar
slf4j-api-1.6.1.jar
slf4j-simple-1.6.1.jar
java_memcached-release_2.6.6.jar这个包在maven依赖引入不管用,我是自己想办法解决的。就是上面遇到问题。
(2).创建一个MemcachedUtil工具类。
用来获取MemcachedClient以及自定义方法。设置连接池的属性,后面我改了是放在用Spring整合了配置。
public class MemcachedUtil {
private static MemCachedClient mcc =new MemCachedClient();
private MemcachedUtil(){
}
static{
String[] servers = {"120.25.222.150:11211"};
Integer[] weights = {1};
SockIOPool pool = SockIOPool.getInstance();
pool.setServers(servers);
pool.setWeights(weights);
pool.setInitConn(5);//设置开始时每个cache服务器的可用连接数
pool.setMinConn(5);//设置每个服务器最少可用连接数
pool.setMaxConn(250);//设置每个服务器最大可用连接数
pool.setMaxIdle(1000*60*60*6);//设置可用连接池的最长等待时间
//设置连接池维护线程的睡眠时间 ,设置为0,维护线程不启动。维护线程主要通过log输出
pool.setMaintSleep(30);
//设置是否使用Nagle算法,因为通讯数据量通常都比较大(相对TCP控制数据)而且要求响应及时,
// 因此该值需要设置为false(默认是true)
pool.setNagle(false);
pool.setSocketConnectTO(0);//设置socket的读取等待超时值
pool.setSocketTO(3000);
pool.setHashingAlg(3);
// 设置hash算法
// alg=0 使用String.hashCode()获得hash code,该方法依赖JDK,可能和其他客户端不兼容,建议不使用
// alg=1 使用original 兼容hash算法,兼容其他客户端
// alg=2 使用CRC32兼容hash算法,兼容其他客户端,性能优于original算法
// alg=3 使用MD5 hash算法
// 采用前三种hash算法的时候,查找cache服务器使用余数方法。采用最后一种hash算法查找cache
// pool.initialize();// 设置完pool参数后最后调用该方法,启动pool。
}
public static MemCachedClient getMemCachedClient(){
return mcc;
}
/**
* set缓存数据
* @param key 取值的键
* @param value 缓存的字符串类型
*/
public static void set(String key,String value){
mcc.set(key,value);
}
/**
* set缓存数据
* @param key
* @param obj 缓存对象类型
*/
public static void set(String key,Object obj){
mcc.set(key,obj);
}
/**
* replace替换存在key的值,key不存在则替换失败
* @param key
* @param obj
*/
public static void replace(String key,Object obj){
mcc.replace(key, obj);
}
/**
* append在已有的key后追加数据
* @param key
* @param obj
*/
public static void append(String key,Object obj){
mcc.append(key,obj);
}
/**
* get 获取单个数据
* @param key
* @return
*/
public static Object get(String key){
return mcc.get(key);
}
/**
* get获取多个值
* @param keys
* @return 返回map集合
*/
public static Map<String,Object> get(Collection<String> keys){
Map<String,Object> map = new HashMap<>();
for(String s:keys){
map.put(s,mcc.get(s));
}
return map;
}
}
(3).修改自己Service的方法。
从数据库查询的学生数据放入缓存。
public class StudentServiceImpl implements StudentService {
@Resource
private StudentMapper studentMapper;
//定义一个常量key
private static String STUKEY = "studentList";
private static Logger logger = LoggerFactory.getLogger(StudentServiceImpl.class);
@Override
public List<Student> selectAll() {
MemCachedClient mcc = MemcachedUtil.getMemCachedClient();
String key = STUKEY;
List<Student> stuList = null;
ObjectMapper mapper = new ObjectMapper();
//如果缓存没有key,则查询数据库,把值放到缓存中,最后返回数据
//否则key存在,直接从缓存中取数据
if(!mcc.keyExists(key)){
stuList = studentMapper.selectAll();
MemcachedUtil.set(key,stuList);
}else{
stuList = castList(mcc.get(key),Student.class);
}
return stuList;
}
/**
* 使用Class.cast做类型转换,将Object转换成List
* @param obj
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> castList(Object obj, Class<T> clazz)
{
List<T> result = new ArrayList<T>();
if(obj instanceof List<?>)
{
for (Object o : (List<?>) obj)
{
result.add(clazz.cast(o));
}
return result;
}
return null;
}
}
6.扩展思考
(1). memcached的客户端有哪些?
Memcached Client for Java
SpyMemcached
XMemcached
Memcached Client for Java 比 SpyMemcached更稳定、更早、更广泛;
SpyMemcached 比 Memcached Client for Java更高效;
XMemcached 比 SpyMemcache并发效果更好
将memcached服务器放置在防火墙后。
(3). memcached使用场景?
>.非持久化存储:对数据存储要求不高
>.分布式存储:不适合单机使用
>.Key/Value存储:格式简单,不支持List、Array数据格式
(4)不适用memcached的业务场景?
>缓存对象的大小大于1MB。Memcached本身就不是为了处理庞大的多媒体(large media)和巨大的二进制块(streaming huge blobs)而设计的。
>key的长度大于250字符,这也是我们尽力避免的
>业务本身需要的是持久化数据或者说需要的应该是database
7.参考文献
百度百科。拉钩教育。
8.更多讨论
评论