接触过的缓存:redis ,memcached ,ehcache,java 内存缓存
这篇笔记以 redis为主,总结了自己项目中的代码,以及其他人博客中的实例,以及《redis的设计及实现》这本书。喜欢笔记的形式,可以提醒一下自己部分知识点,以及知识体系,这样。
redis
1.启动及检测及部分客户端命令
redis 启动: ./redis-server ../redis.conf (修改一下配置文件)
客户端: ./redis-cli
是否启动: ps -ef |grep redis
连接测试: ping
服务器统计信息: info
性能测试: redis-benchmark -n 1000 1000个请求
String : SET keyname "hello world" GET keyname
HASH: HMSET keyname propOne "value one" propTwo 200
LIST: LPUSH keyList "one" ,LPUSH keyList "two"
SET: SADD keySet "one" , SADD keySet "two"
有序集合: ZADD runoobkey 1 redis
发布,订阅: SUBSCRIBE redisChat ,PUBLISH redisChat "message"
参考:
菜鸟教程 :http://www.runoob.com/redis/redis-java.html
redis命令 :http://www.redis.net.cn/order/
2.java中jedis的使用
地址绑定的问题: bind 127.0.0.1 这边可以注释掉
protected-mode no 将保护关掉
RedisConfig:
/**
* 初始化Redis连接池
*/
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxWaitMillis(MAX_WAIT);
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取Jedis实例
* @return
*/
public synchronized static Jedis getJedis() {
try {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
TestRedis:
public class TestRedis {
private static Jedis jedis;
/**
* redis存储字符串
*/
public void testString() {
//-----添加数据----------
jedis.set("name","chenmanman");
System.out.println(jedis.get("name"));
jedis.append("name", " is my name"); //拼接
System.out.println(jedis.get("name"));
jedis.del("name"); //删除某个键
System.out.println(jedis.get("name"));
//设置多个键值对
jedis.mset("name","chenmanman","age","26","qq","14*******5");
jedis.incr("age"); //进行加1操作
System.out.println(jedis.get("name") + "-" + jedis.get("age") + "-" + jedis.get("qq"));
}
/**
* redis操作Map
*/
public void testMap() {
//-----添加数据----------
Map<String, String> map = new HashMap<String, String>();
map.put("name", "chenmanman");
map.put("age", "27");
map.put("qq", "1491563275");
jedis.hmset("user",map);
//取出user中的name,执行结果:[minxr]-->注意结果是一个泛型的List
//第一个参数是存入redis中map对象的key,后面跟的是放入map中的对象的key,后面的key可以跟多个,是可变参数
// List<String> rsmap = jedis.hmget("user", "name", "age", "qq");
List<String> rsmap = jedis.hmget("user", "name", "age");
System.out.println(rsmap);
//删除map中的某个键值
jedis.hdel("user","age");//删除的是age
System.out.println(jedis.hmget("user", "age")); //因为删除了,所以返回的是null
System.out.println(jedis.hlen("user")); //返回key为user的键中存放的值的个数2
System.out.println(jedis.exists("user"));//是否存在key为user的记录 返回true
System.out.println(jedis.hkeys("user"));//返回map对象中的所有key
System.out.println(jedis.hvals("user"));//返回map对象中的所有value
Iterator<String> iter=jedis.hkeys("user").iterator();
while (iter.hasNext()){
String key = iter.next();
System.out.println(key+":"+jedis.hmget("user",key));
}
}
/**
* jedis操作List
*/
public void testList(){
//开始前,先移除所有的内容
jedis.del("java framework");
System.out.println(jedis.lrange("java framework",0,-1));
// Add the string value to the head (LPUSH) or tail (RPUSH) of the list stored at key.
// If the key does not exist an empty list is created just before the append operation.
// If the key exists but is not a List an error is returned.
//先向key java framework中存放三条数据
jedis.lpush("java framework","spring");
jedis.lpush("java framework","struts");
jedis.lpush("java framework","hibernate");
jedis.lpush("java framework","hibernate"); //可以有重复的
// 再取出所有数据jedis.lrange是按范围取出,
// 第一个是key,第二个是起始位置,第三个是结束位置,jedis.llen获取长度 -1表示取得所有
System.out.println(jedis.lrange("java framework",0,-1));
jedis.del("java framework");
jedis.rpush("java framework","spring");
jedis.rpush("java framework","struts");
jedis.rpush("java framework","hibernate");
System.out.println(jedis.lrange("java framework",0,-1));
}
/**
* jedis操作Set
*/
public void testSet(){
//添加
jedis.sadd("user","chenmanman");
jedis.sadd("user","chenxiaoman");
jedis.sadd("user","kyzeng");
jedis.sadd("user","chenxiaoxiao");
//移除noname
jedis.srem("user","chenxiaoxiao");
System.out.println(jedis.smembers("user"));//获取所有加入的value
System.out.println(jedis.sismember("user", "chenxiaoxiao"));//判断 who 是否是user集合的元素
System.out.println(jedis.srandmember("user"));//随机的
System.out.println(jedis.scard("user"));//返回集合的元素个数
}
public void test() throws InterruptedException {
//jedis 排序
//注意,此处的rpush和lpush是List的操作。是一个双向链表(但从表现来看的)
jedis.del("a");//先清除数据,再加入数据进行测试
jedis.rpush("a", "1");
jedis.lpush("a","6");
jedis.lpush("a","3");
jedis.lpush("a","9");
System.out.println(jedis.lrange("a",0,-1));// [9, 3, 6, 1]
System.out.println(jedis.sort("a")); //[1, 3, 6, 9] //输入排序后结果
System.out.println(jedis.lrange("a",0,-1));
}
public void testRedisPool() {
RedisConfig.getJedis().set("newname", "中文测试");
System.out.println(RedisConfig.getJedis().get("newname"));
}
public static void main(String[] args) {
//这边需要注意的是 数据的清空,FLUSHALL cli端的操作
//或者是 jedis.del("a");//先清除数据,再加入数据进行测试
RedisConfig rc =new RedisConfig();
jedis = RedisConfig.getJedis();
// jedis.set("name","chenmanman");
// System.out.println(jedis.get("name"));//执行结果:xinxin
TestRedis tr =new TestRedis();
// tr.testString();
// tr.testMap();
// tr.testList();
// tr.testSet();
try {
tr.test();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
参考文章:
jedis 的API:http://tool.oschina.net/uploads/apidocs/
3.redis的设计及实现
1.数据结构部分
a.字符串
free :未使用的空间
len :保存的字符串的长度
buf :字符串
1).常数复杂度获取字符串的长度。 len
2).杜绝缓冲区溢出 检查free区的大小
3).减少修改字符串长度时所需的内存重分配的次数 空间预分配:多分配一部分 惰性空间释放:使用free接收
b.链表
head : 表头指针
tail : 表尾指针
len : 链表长度计数器
dup,free,match 成员则是用于实现多态链表所需的类型特定函数(复制,释放,比较)
c.字典
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组指定的索引上面。

当键被分配到同一个索引上的时候,就是键冲突,使用链表解决。

rehash操作:实现哈希表的拓展和收缩
拓展,迁移之后,将ht[1]设置为ht[0],然后为ht[1]分配一个空白哈希表

渐进式 rehash
d.跳跃表
header: 指向跳跃表的表头节点
tail: 指向跳跃表的表尾节点
level: 层数最大的节点的层数
length: 包含的节点数量(表头节点不算)
层: 前进指针 和 后退指针 跨度:指向和当前的节点间的距离 计算排位
分值: 节点中保存分值 跳跃表中,节点按照各自所保存的分值从小到大排列
成员对象: 保存的成员对象

整数集合 intset
encoding : 底层整数集合的实现类型
length : 长度
contents : 数据从小到大排列
升级的三步骤
1.根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
2.将底层的所有元素换成与新元素相同的类型,并将转换后的新元素放到正确的位置,保持有序性不变
3.将新元素添加到底层数组里面
压缩列表

压缩列表节点的构成 : previous_entry_length (前一个节点的长度,可以根据当前节点计算出前一个节点的起始位置) ,encoding (数据类型及范围长度), content
连锁更新
对象
对象 : type属性, 编码属性, ptr属性
类型 : 对象的类型 value 保存的数据的类型
编码 : 对象使用了什么底层数据结构作为对象的底层实现

单机数据库的实现

切换数据库: 客户端执行 select 2
数据库数据的添加

过期时间
键的生存时间和过期时间 EXPIRE PEXPIRE设置过期时间 TTL PLLT返回剩余时间
redisDb 结构的expires 字典保存了数据库中的所有的过期时间,即过期字典。(key - value结构)

PERSIST 解除键和值在过期字典中的关联。
过期键删除策略:定时删除(定时器),惰性删除(取用时检测并删除,不用的过期键依然保存),定期删除
save 和 bgsave 生成rdb文件时只会保存未过期的数据。rdb文件载入时,只载入未过期的(主服务器模式);rdb文件载入时,全部载入(从服务器模式);
AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库的某个键已过期,但是它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者被定期删除之后,程序会向AOF文件追加一条DEL命令,来显式的记录该键已被删除。
AOF的重写 和生成RDB文件类似,过期不写。
数据库通知:可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库命令的执行情况。
SUBSCRIBE keyevent@0 :del (订阅0号库中所有的del命令)
SUBSCRIBE keyspace@0 :message (订阅0号库中对message的操作)
RDB持久化
RDB持久化:save 和 bgsave
save 命令会阻塞Redis服务进程
bgsave 会派生出一个子进程来负责创建RDB文件 dump.rdb文件
RDB文件的载入工作是在服务器启动时自动执行的,所以redis并没有专门用于载入RDB文件的命令
AOF文件的更新频率通常比RDB文件的更新频率更高,所以:如果服务器开启了AOF功能,那么服务器会优先使用AOF文件来还原数据库状态。只有AOF持久化功能关闭,服务器才会使用RDB文件来还原数据库状态
服务器载入RDB文件时,会一直处于阻塞状态,直到载入工作结束为止。
自动间隔性保存 save 300 10 300秒内对数据库至少进行10次修改,就执行save操作
dirty , lastsave (计数,最新的save的时间)
RDB文件的结构:REDIS(开头),db_version(RDB版本号),databases(0个或多个数据库),EOF(正文部分的结束),check_num(校验位)
od -c dump.rdb 打印RDB文件
dump.rdb 生成文件是在src下,做文件恢复的时候 ,将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。
bgsave 和save 生成的文件在同一个位置,都叫(dump.rdb)
AOF持久化
因为AOF持久化是通过保存被执行的写命令来记录数据库状态的。
AOF文件的写入与同步: 数据先在aof_buf缓冲区,然后再同步到AOF文件中去 (always, everysec, no)
AOF文件的载入与数据的还原: 将之前的命令重新执行了一遍
AOF文件重写: 直接读取数据库状态
子进程处理数据的时候,主线程还在数据处理,会造成数据库状态不一致的情况
在AOF重写期间,服务器进程执行三个操作:执行客户端发过来的操作;将执行后的命令追加到aof缓冲区;将执行过的命令追加到aof重写缓冲区;
重写结束之后,将aof重写缓冲区的内容写入新的aof文件中,用新的aof文件替换老的AOF文件。
命令请求会保存到aof缓存区里面,之后再定期写入并同步到AOF文件中
服务器只要载入并重新执行保存在aof文件中的命令,就可以还原数据库本来的状态
事件
文件事件:服务器对套接字操作的抽象

I/O多路复用程序通过队列向文件事件分派器传送套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。
时间事件:服务器对定时操作的抽象
定时事件,周期性事件
当时间事件执行器运行的时候,他必须遍历链表中的所有的时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理。
链表并不是按照时间属性的大小排序的
客户端
Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接,当一个连接建立后,Redis 内部会进行以下一些操作:
1.首先,客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。
2.然后为这个 socket 设置 TCP_NODELAY 属性,禁用 Nagle 算法
3.然后创建一个可读的文件事件用于监听这个客户端 socket 的数据发送
服务器
发布和订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
SUBSCRIBE testc :订阅testc
PUBLISH testc “message_one” :发布
UNSUBSCRIBE testc : 退订

事务
一个事务从开始到执行经历的三个阶段:开始事务,命令入队,执行事务
memcached (后端)
brew install memcached
指定端口启动 memcached -p 8000
后台启动 memcached -d -p 8000
memcache (前端)
java-memcached :jar包的生成 maven 打包
测试代码 :
package testmodel;
import com.danga.MemCached.MemCachedClient;
import com.danga.MemCached.SockIOPool;
public class MyCache {
public static void main(String[] args) { //这个是memcache的一个测试的demo,之后可以考虑的是用aop去实现它
MemCachedClient client=new MemCachedClient();
String [] addr ={"127.0.0.1:8000"};
Integer [] weights = {3};
SockIOPool pool = SockIOPool.getInstance();
pool.setServers(addr);
pool.setWeights(weights);
pool.setInitConn(5);
pool.setMinConn(5);
pool.setMaxConn(200);
pool.setMaxIdle(1000*30*30);
pool.setMaintSleep(30);
pool.setNagle(false);
pool.setSocketTO(30);
pool.setSocketConnectTO(0);
pool.initialize();
//String [] s =pool.getServers();
//client.setCompressEnable(true);
//client.setCompressThreshold(1000*1024);
//将数据放入缓存
TestBean bean=new TestBean();
bean.setName("name1");
bean.setAge(25);
client.add("bean1", bean);
//获取缓存数据
TestBean beanClient=(TestBean)client.get("bean1");
System.out.println(beanClient.getName());
}
}
ehcache
ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<cache name="serviceCache"
maxElementsInMemory="20000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
ehcache.xsd 配置略
测试代码:用的切面
@Around("@annotation(cacheable)")
public Object cacheable(final ProceedingJoinPoint pjp, final Cacheable cacheable) throws Throwable {
if(cacheable.cacheOperation() == CacheOperation.NOP) {
return pjp.proceed();
}
String group = cacheable.group().getName(),
cacheKey = getCacheKey(pjp);
Cache cache = getCache(group);
Object result = null;
if(cacheable.cacheOperation() == CacheOperation.CACHING) {
Element element = cache.get(cacheKey);
if(element == null) {
result = pjp.proceed();
if(result == null || result instanceof Serializable) {
element = new Element(cacheKey, (Serializable)result);
LOG.info("正在缓存{0}对象。", cacheKey);
cache.put(element);
}else {
LOG.warn("{0}指定方法返回对象{1}不能被序列化,不能缓存。", cacheKey, result);
}
}else {
LOG.info("从缓存中获取{0}对象。", cacheKey);
result = element.getObjectValue();
}
}else if(cacheable.cacheOperation() == CacheOperation.FLUSH_CACHE) {
result = pjp.proceed();
for(Class<? extends Entity> clazz : cacheable.flushGroup()) {
String groupName = clazz.getName();
if(!groupName.equals(group)) {
LOG.info("删除缓存组{0}所有的缓存对象。", groupName);
getCache(group).removeAll();
}
}
LOG.info("删除缓存组{0}所有的缓存对象。", group);
cache.removeAll();
}
return result;
}
java 内存缓存 :
测试代码:
private CacheManager cacheManager = MapCacheManager.getInstance();
Cache cache = cacheManager.nativeCache(“failPushContent”);
实现类:
public class MapCacheManager implements CacheManager{
private ConcurrentHashMap<String, MapCache> cacheMap = new ConcurrentHashMap<String, MapCache>(0);
private static Logger logger = LoggerFactory.getLogger(MapCacheManager.class);
private static MapCacheManager manager;
private static final long STTLONG = 1000L*60*60*24*365;
private MapCacheManager(){
}
public static MapCacheManager getInstance(){
synchronized (MapCacheManager.class) {
if(null == manager)
manager = new MapCacheManager();
return manager;
}
}
@Override
public List<String> names() throws CacheException {
return new ArrayList<String>(cacheMap.keySet());
}
@Override
public Cache nativeCache(String cacheName) throws CacheException {
return nativeCache(cacheName,STTLONG);
}
@Override
public Cache nativeCache(String cacheName,long expiredTime) throws CacheException {
synchronized (cacheMap){
if(!cacheMap.containsKey(cacheName)){
cacheMap.put(cacheName, new MapCache(cacheName));
logger.info("********nativeCache******: no containsKey");
}
logger.info("********nativeCache******: out");
MapCache mapCache = cacheMap.get(cacheName);
mapCache.setExpiredTime(expiredTime);
return mapCache;
}
}
@Override
public void clear() throws CacheException {
cacheMap.clear();
}
}
MapCache:
public class MapCache implements Cache{
private static final long EXPIREDTIME = 1000L*60*60*24*365;
private static final int param1 = 10;
private final ConcurrentHashMap<Object, Object> store;
private long expiredTime;
public void setExpiredTime(long expiredTime) {
this.expiredTime = expiredTime;
}
private final String name;
private CacheEntity cacheEntity;
private String encryptKey;
/**
* @param name 缓存名称
*/
public MapCache(String name){
this(name,EXPIREDTIME,new ConcurrentHashMap<Object,Object>(param1));
}
/**
* @param name 缓存名称
* @param expiredTime 缓存过期时间
*/
public MapCache(String name,long expiredTime){
this(name,expiredTime,new ConcurrentHashMap<Object,Object>(param1));
}
/**
* @param name 缓存名称
* @param expiredTime 缓存过期时间
* @param store 缓存存储器
*/
public MapCache(String name, long expiredTime, ConcurrentHashMap<Object, Object> store){
this.store = store;
this.expiredTime = expiredTime;
this.name = name;
}
/**
* 返回当前缓存姓名
*/
public String name(){
return this.name;
}
/**
* 添加对象到缓存中
*/
@Override
public void put(Object value,Object... key) throws CacheException {
cacheEntity = new CacheEntity(value);
encryptKey = generateKey(key);
store.put(encryptKey, cacheEntity);
}
/**
* 获得指定键的缓存
*/
@Override
public Object get(Object... key) throws CacheException {
encryptKey = generateKey(key);
cacheEntity = (CacheEntity)store.get(encryptKey);
if(cacheEntity == null){
return "noValue";
}
if(System.currentTimeMillis() - cacheEntity.getTimeInMillIs() > expiredTime){
store.remove(encryptKey);
return "noValue";
}
return cacheEntity.getCacheObject();
}
@Override
public Object getOut(Object... key) throws CacheException {
encryptKey = generateKey(key);
cacheEntity = (CacheEntity)store.get(encryptKey);
if(cacheEntity == null){
return "noValue";
}
if(System.currentTimeMillis() - cacheEntity.getTimeInMillIs() > expiredTime){
store.remove(encryptKey);
return "noValue";
}
Object re= cacheEntity.getCacheObject();
store.remove(encryptKey);//清空
return re;
}
@Override
public void remove(Object... key) throws CacheException {
encryptKey = generateKey(key);
store.remove(encryptKey);
}
@Override
public boolean containsKey(Object... key) throws CacheException {
return store.containsKey(generateKey(key));
}
/**
* 生成缓存加密键
* @param key
* @return
*/
private String generateKey(Object... key){
Assert.isTrue(null != key, "缓存键值不可为空");
StringBuilder result = new StringBuilder();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(JSON.toJSONString(new KeyEntity(key)).getBytes());
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1)
result.append("0");
result.append(hex);
}
return result.toString();
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return result.toString();
}
class KeyEntity{
private List<Object> keys = new ArrayList<Object>();
KeyEntity(Object... keys){
this.keys.addAll(Arrays.asList(keys));
}
public List<Object> getKeys() {
return keys;
}
public void setKeys(List<Object> keys) {
this.keys = keys;
}
}
}