1.Redis 基本介绍
1.1Redis设置外网访问
• 注释bind并且把protected-mode no
• 使用bind 0.0.0.0
• 设置密码  protected-mode它启用的条件有两个,第一是没有使用bind,第二是没有设置访问密码。
1.2Redis通用命令
• keys * :获取所有的key(*代表任意多个字符,?代表任意1个字符)
ps:生产环境一定不用,用scan--根据游标查询一定数量,可匹配的key值。
• del key key2... :删除key
• exists key:判断是否存在
• rename key newkey
• expire key 20:设置20秒后过期
• ttl key:获取key的剩余过期时间(-1代表没设置,-2代表过期)
• type key
• move key 1:将本库的key剪切到1号库中
1.3string对象常用命令(字符串)
赋值
• set key value
• append key value 如果key存在,则在指定的key末尾添加,如果key存在则类似set
• strlen key 返回此key的长度
• setrange key 1(开始位置,从哪里开始设置) 具体值 设置(替换)指定区间范围内的值
• setex 键 秒值 真实值    设置带过期时间的key,动态设置。
• setnx key value 只有在 key  不存在时设置 key 的值
• mset key1 value key2 value 同时设置一个或多个 key-value 对。
• msetnx key1 value key2 value 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
取值
• get key
• getset key value :先获取原值,在设置新值
• getrange key 0(开始位置) -1(结束位置) 获取指定区间范围内的值,(0 -1)表示全部
• mget key1 key 2 获取所有(一个或多个)给定 key 的值。
• getset key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
删除
• del key
数值增减
• incr key :key存在,就在原值上加一,不存在创建一个0然后加1
• decr key
• incrby key increment
• decrby key increment
1.4hash对象常用命令:(字典--散列/hash表+链表结构--两个哈希表用于扩容/缩容)
赋值:
• hset key field value
• hmset key field value [field2 value2...]
取值:
• hget key field
• hmget key fields
• hgetall key 
删除:
• hdel key field [field2 field3...]
• hdel key:删除整个field
增加:
• hincrby key field increment
• hincrdyfloat key key1 数量(浮点数,小数) 执定hash表中某个字段加 数量
• hsetnx key key1 value1 与hset作用一样,区别是不存在赋值,存在了无效。
其他:
• hkeys key
• hvals key
• hlen key
• hexists key field:判断field是否存在
1.5list对象常用命令:(双向链表)
如果键不存在,创建新的链表;如果键已存在,新增内容;如果值全移除,对应的键也就消失了。 
• lpush mylist 1 3 5 7:头插
• rpush mylist 1 3 5 7:尾插
• lrange mylist 0 -1:顺序遍历到最后一个元素(0和-1都代表元素下标)
• lindex key index 通过索引获取列表中的元素
• lpop mylist :返回头元素并弹出
• rpop mylist:返回尾部元素并弹出
• llen mylist:获取元素个数
• lrem key 0(数量) 值,表示删除全部给定的值。零个就是全部值 从left往right删除指定数量个值等于指定值的元素,返回的值为实际删除的数量
• lset mylist 2 x:下标为2的元素设置为x
• linsert mylist before|after x 1:在x元素前或者后插入1
• rpoplpush mylist mylist:把mylist的尾部弹出(会返回元素),插入到头部
1.6set常用命令:(字典-值为null)
同list,如果不存在会自动创建
• sadd key value1 value2 向集合中添加一个或多个成员
• smembers myset:获取key的集合成员
• srem myset a b:删除成员
• scard key:获取成员数量
• sismember key member:判断成员是否在key中,返回0/1
• sdiff key1 key2:差集运算key1-key2 = key1-key1 & key2
• sinter key1 key2:交集运算
• suntion key1 key2:并集运算
• sdiffstore key key1 key2:把key1和key2的差集成员存储在key
• srandmember key 数值 从set集合里面随机取出指定数值个元素 如果超过最大数量就全部取出
• spop key 随机移出并返回集合中某个元素
1.7zset常用命令:(字典+跳表)
zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。 通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
• zadd key score val1 score val2
• zscore key member:返回指定成员的分数(分数存在会进行替换)
• zcard key:返回集合数量
• zcount key start_score end_score 获取分数区间内元素个数
• zrank key vlaue 获取value在zset中的排名(先根据字典查出分数,在使用分数在跳表计算跨度来获取排名)
• zrem key score某个对应值(value),可以是多个值 删除元素
• zrange key start_index end_index:返回索引区间的元素(根据跨度找到start_index 即可)
• zrange key start end with scores:加上分数一起返回
• zrangebyscore key start_score end_score 返回指定score区间的值
• zrevrange key start end with scores:从大到小排序
• zremrangebyrank key start end:根据排名范围进行删除
• zremrangebyscore key start end:根据分数范围进行删除
1.8其他特性
持久化的两种机制AOF和RDB
集群数据的复制同步(全量同步和部分同步),主从备份(主只写可读,从只读),哨兵节点进行故障转移
2.缓存问题
2.1缓存穿透
概念:
• 缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,并且出于容错考虑, 如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
可能造成原因:
• 1.业务代码自身问题
• 2.恶意攻击。爬虫等等
解决方案(缓存空对象):

public class NullValueResultDO implements Serializable{
     private static final long serialVersionUID = -6550539547145486005L;
}
public class UserManager {
     UserDAO userDAO;
     LocalCache localCache;
     public UserDO getUser(String userNick) {
          Object object = localCache.get(userNick);
          if(object != null) {
               if(object instanceof NullValueResultDO) {
                    return null;
               }
               return (UserDO)object;
          } else {
               User user = userDAO.getUser(userNick);
               if(user != null) {
                    localCache.put(userNick,user);
               } else {
                    localCache.put(userNick, new NullValueResultDO());
               }
               return user;
          }
     }          
}
另一种方案:使用布隆过滤器,使用userId构建布隆过滤器即可,如果userId不存在就直接过滤掉;
2.3缓存击穿
概念:
• 热点key突然过期,并发量特别高导致同时去数据库取数据重建缓存,数据库在重建缓存的时候,会出现很多线程同时重建的情况,显然只需要一个线程去重建缓存即可。
解决方案(分布式互斥锁):
public String getKey(String key){
    String value = redis.get(key);
    if(value == null){
        String mutexKey = "mutex:key:"+key; //设置互斥锁的key
        if(redis.set(mutexKey,"1","ex 180","nx")){ //给这个key上一把锁,ex表示只有一个线程能执行,过期时间为180秒
          value = db.get(key);
          redis.set(key,value);
          redis.delete(mutexKety);
        }else{
            // 其他的线程休息100毫秒后重试
            Thread.sleep(100);
            getKey(key);
      }else{
        // 其他的线程休息100毫秒后重试
        Thread.sleep(100);
        getKey(key);
  }
 }
 return value;
}
2.4缓存雪崩
概念:
• 机器宕机或在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案:
• key的过期时间设置的方差太小,可以优化方差来弱化该问题。
2.5数据一致性问题
问题:
• 缓存的数据和数据库的数据不一致;
• 更新数据库和更新缓存,不管是先更新谁,由于不是原子操作所以在多线程场景下一定会出现数据不一致的情况;
解决方案:
• 延时双删--写线程会先删除缓存,然后更新了数据库,再删除一次缓存就是为了防止中间的读操作缓存了旧数据;
• 假设没有并发写,如果有并发写情况,一定会出现数据版本覆盖问题;
• 严格串行化,使用单线程;
3.Redis网络模型
3.1Java--NIO对应的I/O模型
根据操作系统来判断
• 如果是windows用的是select多路复用模型,
• 如果是linux用的是epoll模型,
• 如果是solaris用的是poll模型,
实现的一个非阻塞IO
• 包含:一个服务器端口监听套接字、一个服务器数据传输套接字、一个客户端数据传输套接字
List<SocketChannel> list = new ArrayList();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
	ServerSocketChannel ssc = ServerSocketChannel.open();
	ssc.bind(new InetSocketAddress(9091));//端口默认可复用
	ssc.configureBlocking(false); 
	while (true){ //设置服务器的监听套接字非阻塞
		SocketChannel socketChannel = ssc.accept();
		if(socketChannel==null){
			Thread.sleep(1000);
			System.out.println("没人连接");
			for (SocketChannel channel : list) {
				int k =channel.read(byteBuffer);
				System.out.println(k);
				if(k!=0){
					byteBuffer.flip();
					System.out.println(new String(byteBuffer.array()));
				}
			}
		}else{ //设置客户端read数据传输套接字非阻塞
			socketChannel.configureBlocking(false);
			list.add(socketChannel);
			//得到套接字,循环所有的套接字,通过套接字获取数据
			for (SocketChannel channel : list) {
				int k =channel.read(byteBuffer);
				System.out.println(k+"=======================================");
				if(k!=0){
					byteBuffer.flip();
					System.out.println(new String(byteBuffer.array()));
				}
			}
		}
	}
} catch (IOException e) {
	e.printStackTrace();
} catch (InterruptedException e) {
	e.printStackTrace();
}
3.2NIO实现一之select多路复用模型
文件描述符与FILE结构体介绍
• 文件描述符是文件描述符表这个数组的索引,FILE结构体相当于文件内容,比如socket就是ip+端口的文件,创建成功会返回一个文件描述符(listenfd = socket(AF_INET,SOCK_STREAM,0))。
• 当我们创建一个进程时,会创建文件描述符表,进程控制块PCB中的fs指针指向文件描述符表,
当我们创建一个文件时,会为指向该文件的指针FILE*关联一个文件描述符并添加在文件描述符表中。• 对于文件描述符表来说fd相当于数组的索引,FILE*相当于数组的内容,指向一个文件结构体。文件描述符表默认设置大小为1024,所以select最多只能建立1024个socket连接。
• 文件描述符表前三位被占用了,分别是标准输入,标准输出和标准错误;第四位为服务端的监听套接字,所以最多只有1024-4=1020个客户端同时连接。
select源码分析
void start(){
	//定义一个结构体(网络地址结构体)主要在listenfd建立的时候用
	struct sockaddr_in my_addr; 
	my_addr.sin_family = AF_INET; // ipv4
	my_addr.sin_port   = htons(8080);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	//定义一个结构体(网络地址结构体)主要在accept的时候传出客户端的信息
	struct sockaddr_in client_addr;		   
	char cli_ip[INET_ADDRSTRLEN] = "";	   
	//fd,与客户端通信
	int clientfd = 0;
	//得到一个listen的fd文件描述符
	int listenfd = socket(AF_INET,SOCK_STREAM,0);
	//绑定一个端口+ip,所以我们可以认为一个socket就是一个ip+端口的网络进程或者文件
	bind(listenfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	//设置同时能够接受请求为128个,注意是同一时间
	listen(listenfd, 128);
	printf("listen client @port=%d...\n",8080);
	//参数定义
	int lastfd = listenfd;
	int i;
	//位图,姑且理解为集合,能够监听到R事件
	fd_set readset,totalSet;
	//先清空把位图的值都改为0
	FD_ZERO(&readset);
	//listenfd 表示listenfd我需要监听他的R事件
	FD_SET(listenfd, &totalSet);
	while(1)
		{
			readset = totalSet;
			//setup 1 把监听的套接字和客户端的套接字给select
			int z = select(lastfd+1,&readset,NULL,NULL,NULL);
			//判断是否有事件发生
			if(z>0){
				//如果有事件发生,其实我并不知道是什么事件,但是有两种情况
				if(FD_ISSET(listenfd,&readset)){
					socklen_t cliaddr_len = sizeof(client_addr); //因为他是传入传出,会覆盖
					clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);
					inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
					printf("----------------------------------------------\n");
					printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
					FD_SET(clientfd, &totalSet);	
					lastfd=clientfd;
					if(0==--z){
						continue;
					}
				}
				for(i=listenfd+1;i<=lastfd;i++){//处理客户端发过来的消息
					if(FD_ISSET(i,&readset)){//如果第I个fd有R事件,那么直接把内容读出来
						char recv_buf[512] = "";
						int rs=read(i,recv_buf,sizeof(recv_buf));
						if(rs==0){//客户端关闭
							close(i);
							FD_CLR(i,&totalSet);
						}else{
							printf("%s\n",recv_buf);
							//write(0,recv_buf,rs);
						}
					}
				}

			}
		}
}