一、简单介绍

redis最主要的功能:
	1.1 做缓存,一些经常查询的数据又不会修改的数据就可以放在redis中;
	1.2 可以做redis队列,比如短信邮件可以放入到redis队列中。
	1.3 redis-session共享
	1.4 redis-锁 
	缺点:数据多了会消耗内存,
	优点:但是查询数据库快,可以减轻数据库的压力。
	首先redis有16个数据库,那么根据数据库的下标去区分数据到底存入到那个数据库。数据是保存在内存中的,
	Redis是一个高效的内存数据库,他所支持包括 String、List 、Set、SotreSet 和Hash等数据类型的存储。
	在redis通常根据key查询value值

二、redisRedsi缓存雪崩、缓存穿透、数据库和redis一致性等问题

2.1缓存雪鹏

定义:
	当 redis服务挂掉时,大量请求数据库,对数据库产生巨大的压力,导致数据库瘫痪。
场景:
     把所有存入redis的所有数据设置相同过期的时间,过期时间失效后,就会大量请求数据库。
如何解决?
   1、在缓存的时候我们给过期时间设置一个随机数,但是也要根据业务场景需求来设置
   2、事发前:实现redis的高可用、主从架构+sentinel 或者 redis cluster
   3、事发后:万一redis真的挂了、可以设置本地缓存ehcache+限流hystrix
   4、事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据

2.2 缓存穿透:

定义:
	大量到数据库请求一些不存在的数据,查询一个数据库不存在的数据。请求的数据在缓存大量不命中,导致大量请求数据库。
场景:
	比如我们表的数据的id是从1开始的正数,如果在这里有黑客攻击我们的系统,会设置一些负数的id到数据库查询数据,
	查询出来返回的数据为null,在这里,由于缓存不命中,并且处于容错考虑,从数据库查询的数据为null就不写到redis,
	这将导致每次查询数据都会到数据库查询,失去了缓存的意义。这样数据库迟早也会挂掉
如何解决缓存穿透?
    1、由于请求的参数是不合法(-1) 每次请求都是不存在的数据,于是我们可以使用布隆过滤器(BloomFilter) 或者 压缩filter提前拦截,
    不合法就不能访问数据库。
    2、当我们从数据库查询出来的数据为null时,也把他设置到缓存redis中,下次请求的时候就到redis中查询了,
    在这里后台可以判断,如果为null,那么设置一个较短的过期时间,到期自动就失效,否则就是按正常操作。

2.3 缓存与数据库双写一致性:

对于读操作流程:
     先到redis缓存中查询数据,如果为null,那么再到数据库查询出来再设置到redis中去,最后将数据返回给请求。
定义:
   如果只是简单查询,缓存数据和数据库数据没有什么问题,当我们更新的时候就可能导致数据库数据和缓存数据不一致了。
   数据库库存为 999  缓存数据为1000 这就是更新导致的情况。	
解决方案:
    1、比如操作菜单的时候,当我们增加 、删除、修改菜单时,操作成功之后就应该立刻根据菜单的key从redis缓存中把数据给删除,
  第二次查询 的时候肯定为null,从数据库查询再设置到redis中。这是马上见效情况,
    2、不是马上见效的情况,就是设置过期时间来确定,比如我们商城中web页面根据店铺搜索出来的数据有最新的4张照片
  当我们在商家后台添加一个商品时,就应该显示在最新添加的照片,此时就不能按照删除key来操作redis了,因为多个商家添加多个商品,
  就失去了缓存的意义,那么会根据用户需求来设置过期时间,这里的redis缓存就可能和数据库不一致,需要过期时间来控制数据。
  因为缓存时间到了,就会被删除,再到数据库查询设置到redis中去。

2.3.1高并发情况下:

操作一:
先更新数据库,再删除缓存
   正常的情况是这样的:
      1、 先操作数据库,成功
      2、再删除缓存,也成功
      3、 如果原子性被破坏了
  第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据。
  如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
  如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
       缓存刚好失效
       线程A查询数据库,得一个旧值
       线程B将新值写入数据库
       线程B删除缓存
       线程A将查到的旧值写入缓存
   要达成上述情况,还是说一句概率特别低:
       因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,
      而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率
      基本并不大。对于这种策略,其实是一种设计模式:Cache Aside Pattern
解决一:
	删除缓存失败的解决思路:
		将需要删除的key发送到消息队列中
		自己消费消息,获得需要删除的key
		不断重试删除操作,直到成功
	
	
操作二:	
		先删除缓存,再更新数据库
			正常情况是这样的:
			  1、先删除缓存,成功;
			  2、再更新数据库,也成功;
			  3、如果原子性被破坏了:
			第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的。
			如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。
			看起来是很美好,但是我们在并发场景下分析一下,就知道还是有问题的了:
				 线程A删除了缓存
				 线程B查询,发现缓存已不存在
				 线程B去数据库查询得到旧值
				 线程B将旧值写入缓存
				 线程A将新值写入数据库
		 所以也会导致数据库和缓存不一致的问题。
解决二:
			并发下解决数据库与缓存不一致的思路:
			将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。

redis 如何制定分库 redis 分库作用_数据

对比两种策略:

	先删除缓存、再更新数据库
			在高并发下表现的不如意,在原子性被破环时表现优异
	先更新数据库,再删除缓存(Cache Aside Pattern设计模式))
			在高并发先表现优异,在原子性被破坏时表现不如意

三、redis分页加排序操作

介绍:
	Redis是一个高效的内存数据库,他所支持包括 String、List 、Set、SotreSet 和Hash登数据类型的存储。在redis通常根据key查询
	value值,redis没有条件查询,在面对一些需要分页或者排序的场景时(培训 时间线)redis就不太好处理。
	
项目中案列使用:
      需要将每个主题下的用户的评论组装好写到redis中,每个主题会有一个topicId,每一条评论会和topicid关联起来,
  得到的大致数据模型为{ topicId: 'xxxxxxxx', comments: [ { username: 'niuniu', createDate: 1447747334791, 
  content: '在Redis中分页', commentId: 'xxxxxxx', reply: [ { content: 'yyyyyy' username: 'niuniu' }, ... ] }, ... ]}
      将评论数据从mysql查询出来组装好存入到redis后,从上面的数据可以看出都是key value形式,所以会使用到hash
   进行存储, hash肯定是不能分页的,那么可能会用到StoreSet
   
   五大类型介绍:
		1、String:主要用于存储字符串、显然不支持分页排序
		2、Hash: 主要用于存储key-value形式数据,评论模型中全是key-value,所以会使用到
		3、Set:  主要存储无序集合  无序 排除
		
		4、stortSet :主要存储有序集合,StortSet添加元素指令Zadd  key  score member [[score , member] ] 
		会给每个元素的member绑定一个用于排序的值score , StroreSet会很据score的值的大小进行排序,
		在这里可以将一个需要排序的字段当作score排序,StoreSet指令中的Zervrange  key  start stop由可以返回指定区间的成员,
		意思就是可以用来分页 , 还有一个好处就是SortedSet的指令Zerm  key member 可以根据key移除指定的成员,
		可以满足删除评论的要求,所以 SortedSet是用来分页 的。
		
		5、List: 主要用来存储一个列表,列表中的每一个元素按元素的插入时的顺序进行保存,如果我们将评论模型按字段(排好)放入redis后再
		插入list中,就可以做到排序,但是其中数据删掉就乱了,list中的lrange  key start stop 指令还能做分页,
		那么单纯使用list也能做到分页排序了,但是在这里,如果评论被删除,就需要到数据库重新查询一次放入到redis中,
		这样的话性能也不好,而且也不太优雅,就需要更新redis中的数据了,如果在这里可以删除指定的数据那就更好了,
		但是list中有lpop,rpop这两个指令,他们只能删除列的表头和表尾的数据,不能指定删除,所以在这里list也不太好,

下图所示:

redis 如何制定分库 redis 分库作用_数据_02

redis 如何制定分库 redis 分库作用_缓存_03

在上图的SortSet结构中将每个主题的topicId作为set的key,将与该主题关联的评论的createDate和commentId分别作为set的score
和member,commentId的顺序就根据createDate的大小进行排列。
     当需要查询某个主题某一页的评论时,就可主题的topicId通过指令zrevrange topicId (page-1)×10   (page-1)×10+perPage这样就能
     找出某个主题下某一页的按时间排好顺序的所有评论的commintId。page为查询第几页的页码,perPage为每页显示的条数。
     当找到所有评论的commentId后,就可以把这些commentId作为key去Hash结构中去查询该条评论对应的内容。
     这样就利用SortSet和Hash两种结构在Redis中达到了分页和排序的目的。

redis 如何制定分库 redis 分库作用_redis 如何制定分库_04

redis 如何制定分库 redis 分库作用_redis_05

四、redis-session操作

四、大家最喜欢的代码操作:

4.1 properties中的配置:

redis.host=127.0.0.1     本机地址 需要开启
	redis.port=6379            redis的端口号
	redis.database=15     redis数据库的下标,有了这个操作就是把数据存入到下标为15的数据库中
	redis.password=20162016  redis密码
	redis.timeout=3000    超时时间

4.2 applicationContext.xml中的配置:

<!--redis中设置了密码 ,默认添加到redis的第一个数据库-->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
    <constructor-arg index="0" ref="jedisPoolConfig"/>
    <constructor-arg index="1" value="${redis.host}" type="java.lang.String" />
    <constructor-arg index="2" value="${redis.port}" type="int"/>
    <constructor-arg index="3" value="${redis.timeout}" type="int"/>
    <constructor-arg index="4" value="${redis.password}" type="java.lang.String"/>
</bean>

<!--指定redis中数据库的小标为 15 -->
<bean id="jedisPool1" class="redis.clients.jedis.JedisPool">
    <constructor-arg index="0" ref="jedisPoolConfig"/>
    <constructor-arg index="1" value="${redis.host}" type="java.lang.String" />
    <constructor-arg index="2" value="${redis.port}" type="int"/>
    <constructor-arg index="3" value="${redis.timeout}" type="int"/>
    <constructor-arg index="4" value="${redis.password}" type="java.lang.String"/>
    <constructor-arg index="5" value="${redis.database}" type="int"/>
</bean>

4.2 导包pom.xml:

<dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
  </dependency>

五、java代码

5.1 、工具类

@Component
public class RedisHelper {

@Autowired
private JedisPool jedisPool;  //如果注入这个就是默认把数据存入到redis的第一个数据库

@Autowired
private JedisPool jedisPool1;  //如果注入这个就是把数据存入到redis的16个数据库 下标为15
 /**
 * cache前缀
 */
@Value("${cachePrefix}")   //这个写一个前缀 。类似于一个文件夹 在RedisDesktopManager 可视化工具中可以查看
private String cachePrefix;


/**
 * 根据key获取缓存数据
 *
 * @param key
 * @return
 */
public String get(String key) {
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.get(cachePrefix + key);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 获取指定前缀的所有key
 *
 * @param prefix
 * @return
 */
public Set<String> keys(String prefix) {
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.keys(cachePrefix + prefix);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 根据key设置缓存数据,如果以前存在更新,如果以前没有添加
 *
 * @param key
 * @param value
 */
public void set(String key, String value) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.set(cachePrefix + key, value);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 根据key设置缓存数据,如果以前存在更新,如果以前没有添加
 *
 * @param key
 * @param value
 * @param expire 过期时间,单位秒
 */
public void set(String key, String value, int expire) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.set(cachePrefix + key, value);
        jedis.expire(cachePrefix + key, expire);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 根据key删除缓存数据
 *
 * @param key
 */
public void del(String key) {


    Jedis jedis = jedisPool.getResource();
    try {
        jedis.del(cachePrefix + key);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 设置集合   这个的话简单介绍一下,他主要在我们的项目中和es结合使用的,比如订单和商品是发布的es中,因为es查询速度很快
 * 那么可变参数 我们只需要把商品的id和订单的id存入到一个数组中,再调用此方法,到时候再es中根据redis中下面smembers这个方法
 * 拿到所有的id,再到数据库查询出来一个对象存入到es中,到时候商品和订单就是从数据库查询出来的。
 * @param name
 * @param value
 */
public void sadd(String name, String... value) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.sadd(cachePrefix + name, value);

    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 获取集合  
 * @param name
 */
public void scard(String name){
    Jedis jedis = jedisPool.getResource();
    try {
        Long scard = jedis.scard(cachePrefix + name);
        System.out.println("scard=="+scard);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 获取指定键名的集合中的所有成员
 *
 * @param name
 * @return
 */
public Set<String> smembers(String name) {
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.smembers(cachePrefix + name);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 返回集合中的随机元素
 *
 * @param name
 * @param count
 * @return
 */
public List<String> srandmember(String name, int count) {
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.srandmember(cachePrefix + name, count);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 移除集合
 *
 * @param name
 * @return
 */
public void spop(String name) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.spop(cachePrefix + name);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 移除集合中的一个或多个元素
 *
 * @param name
 * @return
 */
public void srem(String name, String... key) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.srem(cachePrefix + name, key);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 添加到hash
 *
 * @param name
 * @param key
 * @param value
 */
public void hset(String name, String key, String value) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.hset(cachePrefix + name, key, value);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 从hash中读取
 *
 * @param name
 * @param key
 * @return
 */
public String hget(String name, String key) {
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.hget(cachePrefix + name, key);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 从hash中删除
 *
 * @param name
 * @param key
 */
public void hdel(String name, String key) {
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.hdel(cachePrefix + name, key);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 设置key的过期时间
 * @param name
 * @param seconds
 */
public void expire(String name, int seconds){
    Jedis jedis = jedisPool.getResource();
    try {
        jedis.expire(cachePrefix + name, seconds);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}

/**
 * 检查键是否存在
 * @param name
 * @return
 */
public Boolean exists(String name){
    Jedis jedis = jedisPool.getResource();
    try {
        return jedis.exists(cachePrefix + name);
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}
}

5.2、redis队列代码

@Component
public class QueueHelper {

    @Autowired
    private JedisPool jedisPool;

    /**
     * Queue前缀
     */
    @Value("${queuePrefix}")
    private String prefix;

    /**
     * 读取队列
     * @return
     */
    public String pop(String name) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.lpop(prefix + name);
        } finally {
            jedisPool.returnResourceObject(jedis);
        }
    }

    /**
     * 写入队列   简单介绍一下:value就是我们发送短信的对象,比如把电话号码 、发布的内容、封转到一个对象中,再把此对象转换为json字符串的		格式传进来,到时候再定时任务中再调上面那个方法查询出来,再去调发布短信和邮件的方法就可以了,注意:此时的定时任务需要配置为一直运行。
     * 
     * @param value
     */
    public void push(String name, String value) {
        Jedis jedis = jedisPool.getResource();
        try {
            jedis.rpush(prefix + name, value);
        } finally {
            jedisPool.returnResourceObject(jedis);
        }
    }
}

5.3、redis锁

package net.shopnc.b2b2c.lbjt;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;

@Component
public class SaveGoodsIdToRedisHelper {


private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

private static final Long RELEASE_SUCCESS = 1L;

private static String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

//在这里 redis锁的单位是毫秒
public static final int EXPIRATIONTIME = 180000;


@Autowired
private JedisPool jedisPool2;

/**
 * Queue前缀
 */
@Value("${queuePrefix}")
private String prefix;

/**
 * 检查键是否存在
 *
 * @param name
 * @return
 */
public Boolean exists(String name) {
    Jedis jedis = jedisPool2.getResource();
    try {
        return jedis.exists(prefix + name);
    } finally {
        jedisPool2.returnResourceObject(jedis);
    }
}


//设置订单锁
public void setOrderLock(String name, String requestId) {
    Jedis jedis = jedisPool2.getResource();
    try {

        String set = jedis.set(name, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRATIONTIME);
        // System.out.println(set);

    } finally {
        jedisPool2.returnResourceObject(jedis);
    }
}



//判断key是否存在
public String getOrderValue(String name) {
    Jedis jedis = jedisPool2.getResource();
    try {
        String s = jedis.get(name);
        return s;
    } finally {
        jedisPool2.returnResourceObject(jedis);
    }

}

//订单解锁
public String getOrderLock(String name, String value) {
    Jedis jedis = jedisPool2.getResource();
    try {

        Object eval = jedis.eval(script, Collections.singletonList(name), Collections.singletonList(value));
        return "";
    } finally {
        jedisPool2.returnResourceObject(jedis);
    }

}
}

5.4、redis分页排序

//redis分页
    public  List<String>  redisPageSize(String key) {

    int pageNo = 2;
    int pageSize = 2;
    Jedis jedis = jedisPool.getResource();
    try {

        int start = pageSize * (pageNo - 1); // 因为redis中list元素位置基数是0  0   6
        int end = start + pageSize - 1;   //5     11  指的是下标


        List<String> results = jedis.lrange(key, start, end);// 从start算起,start算一个元素,到结束那个元素
        for (String str : results) {
            System.out.println("str====" + str);
        }
        return results;
    } finally {
        jedisPool.returnResourceObject(jedis);
    }
}


 	//redis分页+排序
    public void redisQueryPageAndSort() {

    Jedis jedis = jedisPool1.getResource();
    Map<String, String> map = new HashMap<String, String>();
    
    for(int i=0 ; i<10 ; i++){
        jedis.zadd("topicId", i, "name" + i);
        map.put("name"+i , "到底怎么回事"+i);
    }


    jedis.hmset("user", map);

    int currentPage =1;
    int pageSize = 10 ;
    int offset = (currentPage-1)*10;

    LinkedHashSet<String> sets = (LinkedHashSet<String>) jedis.zrevrangeByScore("topicId" ,"80", "1", offset, pageSize);

    String s1 = sets.toString();


    System.out.println(sets.toString());
    Object[] objects = sets.toArray();

    String[] strings = new String[sets.size()];

    for(int i=0 ; i <objects.length ; i++){
        strings[i] = (String)objects[i];
    }

    
    //取出user中的name,执行结果:[minxr]-->注意结果是一个泛型的List

    //第一个参数是存入redis中map对象的key,后面跟的是放入map中的对象的key,后面的key可以跟多个,是可变参数

    List<String> rsmap = jedis.hmget("user", strings);
    for (String s : rsmap) {
        System.out.println(s);
    }

    System.out.println(rsmap);
}