9.30更新:这本书应该不会再看下去了,本来想的是中文书自己没有基础可以一看,但发现干货真的不多,全文都是在贴代码,并且文字有点冗余。人生短暂,及时止损,把时间花在更难更有价值的书籍上吧。

缓存中间件Redis

Redis使用

Redis自定义注入Bean组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CommonConfig {
    /**
     * Redis链接工厂
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 缓存RedisTemplate的自定义配置
     *
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //TODO:指定Key序列化策略为为String序列化,Value为JDK自带的序列化策略
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        //TODO:指定HashKey序列化策略为String序列化-针对Hash散列存储
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    /**
     * 缓存StringRedisTemplate
     *
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        //采用默认配置即可-后续有自定义配置时则在此处添加即可
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }
}

RedisTemplate实战

(1)使用RedisTemplate将字符串信息写入缓存中,并读取出来展示到控制台上。

(2)使用RedisTemplate将对象信息序列化为JSON格式的字符串后写入缓存中,然后将其读取出来,最后反序列化解析其中的内容并展示在控制台上。

StringRedisTemplate实战

(1)使用StringRedisTemplate将字符串信息写入缓存中,并读取出来展示到控制台上。

(2)使用StringRedisTemplate将对象信息序列化为JSON格式的字符串后写入缓存中,然后将其读取出来,最后反序列化解析其中的内容并展示在控制台上。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisTest {

    private static final Logger log = LoggerFactory.getLogger(RedisTest.class);

    @Autowired
    private RedisTemplate redisTemplate;

    //采用RedisTemplate将字符串信息写入缓存中,并读取出来
    @Test
    public void one() {
        log.info("------开始RedisTemplate操作组件实战----");

        //定义字符串内容以及存入缓存的key
        final String key = "redis:template:one:string";
        final String content = "RedisTemplate实战字符串信息";

        //Redis通用的操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();

        //将字符串信息写入缓存中
        log.info("写入缓存中的内容:{} ", content);
        valueOperations.set(key, content);

        //从缓存中读取内容
        Object result = valueOperations.get(key);
        log.info("读取出来的内容:{} ", result);
    }

    @Autowired
    private ObjectMapper objectMapper;

    //采用RedisTemplate将对象信息序列化为JSOn格式字符串后写入缓存中,
    //然后将其读取出来,最后反序列化解析其中的内容并展示在控制台
    @Test
    public void two() throws Exception {
        log.info("------开始RedisTemplate操作组件实战----");

        //构造对象信息
        User user = new User(1, "debug", "阿修罗");

        //Redis通用的操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();

        //将序列化后的信息写入缓存中
        final String key = "redis:template:two:object";
        final String content = objectMapper.writeValueAsString(user);

        valueOperations.set(key, content);
        log.info("写入缓存对象的信息:{} ", user);

        //从缓存中读取内容
        Object result = valueOperations.get(key);
        if (result != null) {
            User resultUser = objectMapper.readValue(result.toString(), User.class);
            log.info("读取缓存内容并反序列化后的结果:{} ", resultUser);
        }
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //采用StringRedisTemplate将字符串信息写入缓存中,并读取出来
    @Test
    public void three() {
        log.info("------开始StringRedisTemplate操作组件实战----");

        //定义字符串内容以及存入缓存的key
        final String key = "redis:three";
        final String content = "StringRedisTemplate实战字符串信息";

        //Redis通用的操作组件
        ValueOperations valueOperations = stringRedisTemplate.opsForValue();

        //将字符串信息写入缓存中
        log.info("写入缓存中的内容:{} ", content);
        valueOperations.set(key, content);

        //从缓存中读取内容
        Object result = valueOperations.get(key);
        log.info("读取出来的内容:{} ", result);
    }

    //采用StringRedisTemplate将对象信息序列化为JSON格式字符串后写入缓存中,
    //然后将其读取出来,最后反序列化解析其中的内容并展示在控制台
    @Test
    public void four() throws Exception {
        log.info("------开始StringRedisTemplate操作组件实战----");

        //构造对象信息
        User user = new User(2, "SteadyJack", "阿修罗");

        //Redis通用的操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();

        //将序列化后的信息写入缓存中
        final String key = "redis:four";
        final String content = objectMapper.writeValueAsString(user);

        valueOperations.set(key, content);
        log.info("写入缓存对象的信息:{} ", user);

        //从缓存中读取内容
        Object result = valueOperations.get(key);
        if (result != null) {
            User resultUser = objectMapper.readValue(result.toString(), User.class);
            log.info("读取缓存内容并反序列化后的结果:{} ", resultUser);
        }
    }
}

Redis数据结构

字符串String

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//字符串类型
@Test
public void one() throws Exception{
    //构造用户个人实体对象
    Person person=new Person(10013,23,"修罗","debug","火星");

    //定义key与即将存入缓存中的value
    final String key="redis:test:1";
    String value=objectMapper.writeValueAsString(person);

    //写入缓存中
    log.info("存入缓存中的用户实体对象信息为:{} ",person);
    redisTemplate.opsForValue().set(key,value);

    //从缓存中获取用户实体信息
    Object res=redisTemplate.opsForValue().get(key);
    if (res!=null){
        Person resP=objectMapper.readValue(res.toString(),Person.class);
        log.info("从缓存中读取信息:{} ",resP);
    }
}

列表List

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//列表类型
@Test
public void two() throws Exception {
    //构造已经排好序的用户对象列表
    List<Person> list = new ArrayList<>();
    list.add(new Person(1, 21, "修罗", "debug", "火星"));
    list.add(new Person(2, 22, "大圣", "jack", "水帘洞"));
    list.add(new Person(3, 23, "盘古", "Lee", "上古"));
    log.info("构造已经排好序的用户对象列表: {} ", list);

    //将列表数据存储至Redis的List中
    final String key = "redis:test:2";
    ListOperations listOperations = redisTemplate.opsForList();
    for (Person p : list) {
        //往列表中添加数据-从队尾中添加
        listOperations.leftPush(key, p);
    }

    //获取Redis中List的数据-从队头中获取
    log.info("--获取Redis中List的数据-从队头中获取--");
    Object res = listOperations.rightPop(key);
    Person resPerson;
    while (res != null) {
        resPerson = (Person) res;
        log.info("当前数据:{} ", resPerson);
        res = listOperations.rightPop(key);
    }
}

Redis的列表List类型特别适用于“排名”“排行榜”“近期访问数据列表”等业务场景。

集合Set

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//集合类型
@Test
public void three() throws Exception {
    //构造一组用户姓名列表
    List<String> userList = new ArrayList<>();
    userList.add("debug");
    userList.add("jack");
    userList.add("修罗");
    userList.add("大圣");
    userList.add("debug");
    userList.add("jack");
    userList.add("steadyheart");
    userList.add("修罗");
    userList.add("大圣");

    log.info("待处理的用户姓名列表:{} ", userList);

    //遍历访问,剔除相同姓名的用户并塞入集合中,最终存入缓存中
    final String key = "redis:test:3";
    SetOperations setOperations = redisTemplate.opsForSet();
    for (String str : userList) {
        setOperations.add(key, str);
    }

    //从缓存中获取已剔除的用户集合
    Object res = setOperations.pop(key);
    while (res != null) {
        log.info("从缓存中获取的用户集合-当前用户:{} ", res);
        res = setOperations.pop(key);
    }
}

在实际应用中,Redis的Set类型常用于解决重复提交、剔除重复ID等业务场景。

有序集合SortedSet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//有序集合
@Test
public void four() throws Exception {
    //构造一组无序的用户手机充值对象列表
    List<PhoneUser> list = new ArrayList<>();
    list.add(new PhoneUser("103", 130.0));
    list.add(new PhoneUser("101", 120.0));
    list.add(new PhoneUser("102", 80.0));
    list.add(new PhoneUser("105", 70.0));
    list.add(new PhoneUser("106", 50.0));
    list.add(new PhoneUser("104", 150.0));
    log.info("构造一组无序的用户手机充值对象列表:{}", list);

    //遍历访问充值对象列表,将信息塞入Redis的有序集合中
    final String key = "redis:test:4";
    //因为zSet在add元素进入缓存后,下次就不能进行更新了,故而为了测试方便,
    //进行操作之前先清空该缓存(当然实际生产环境中不建议这么使用)
    redisTemplate.delete(key);

    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    for (PhoneUser u : list) {
        zSetOperations.add(key, u, u.getFare());
    }

    //前端获取访问充值排名靠前的用户列表
    Long size = zSetOperations.size(key);
    //从小到大排序
    Set<PhoneUser> resSet = zSetOperations.range(key, 0L, size);
    //从大到小排序
    //Set<PhoneUser> resSet=zSetOperations.reverseRange(key,0L,size);
    for (PhoneUser u : resSet) {
        log.info("从缓存中读取手机充值记录排序列表,当前记录:{} ", u);
    }
}

Redis的有序集合SortedSet常用于充值排行榜、积分排行榜、成绩排名等应用场景。

哈希Hash存储

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//Hash哈希存储
@Test
public void five() throws Exception {
    //构造学生对象列表,水果对象列表
    List<Student> students = new ArrayList<>();
    List<Fruit> fruits = new ArrayList<>();

    students.add(new Student("10010", "debug", "大圣"));
    students.add(new Student("10011", "jack", "修罗"));
    students.add(new Student("10012", "sam", "上古"));

    fruits.add(new Fruit("apple", "红色"));
    fruits.add(new Fruit("orange", "橙色"));
    fruits.add(new Fruit("banana", "黄色"));

    //分别遍历不同对象队列,并采用Hash哈希存储至缓存中
    final String sKey = "redis:test:5";
    final String fKey = "redis:test:6";

    HashOperations hashOperations = redisTemplate.opsForHash();
    for (Student s : students) {
        hashOperations.put(sKey, s.getId(), s);
    }
    for (Fruit f : fruits) {
        hashOperations.put(fKey, f.getName(), f);
    }

    //获取学生对象列表与水果对象列表
    Map<String, Student> sMap = hashOperations.entries(sKey);
    log.info("获取学生对象列表:{} ", sMap);

    Map<String, Fruit> fMap = hashOperations.entries(fKey);
    log.info("获取水果对象列表:{} ", fMap);

    //获取指定的学生对象、水果对象
    String sField = "10012";
    Student s = (Student) hashOperations.get(sKey, sField);
    log.info("获取指定的学生对象:{} -> {} ", sField, s);

    String fField = "orange";
    Fruit f = (Fruit) hashOperations.get(fKey, fField);
    log.info("获取指定的水果对象:{} -> {} ", fField, f);
}

在实际互联网应用,当需要存入缓存中的对象信息具有某种共性时,为了减少缓存中Key的数量,应考虑采用Hash哈希存储。

Key失效

1、调用SETEX方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Key失效一
@Test
public void six() throws Exception {
    //构造key与redis操作组件
    final String key1 = "redis:test:6";
    ValueOperations valueOperations = redisTemplate.opsForValue();

    //第一种方法:在往缓存中set数据时,提供一个TTL,表示TTL时间一到,缓存中的key将自动失效,即被清理
    //在这里TTL是10秒
    valueOperations.set(key1, "expire操作1", 10L, TimeUnit.SECONDS);

    //等待5秒-判断key是否还存在
    Thread.sleep(5000);
    Boolean existKey1 = redisTemplate.hasKey(key1);
    Object value = valueOperations.get(key1);
    log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);

    //再等待5秒-再判断key是否还存在
    Thread.sleep(5000);
    existKey1 = redisTemplate.hasKey(key1);
    value = valueOperations.get(key1);
    log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);
}

2、使用RedisTemplate操作组件的Expire()方法指定失效的Key

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Key失效二
@Test
public void seven() throws Exception {
    //构造key与redis操作组件
    final String key2 = "redis:test:7";
    ValueOperations valueOperations = redisTemplate.opsForValue();

    //第二种方法:在往缓存中set数据后,采用redisTemplate的expire方法失效该key
    valueOperations.set(key2, "expire操作2");
    redisTemplate.expire(key2, 10L, TimeUnit.SECONDS);

    //等待5秒-判断key是否还存在
    Thread.sleep(5000);
    Boolean existKey = redisTemplate.hasKey(key2);
    Object value = valueOperations.get(key2);
    log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey, value);

    //再等待5秒-再判断key是否还存在
    Thread.sleep(5000);
    existKey = redisTemplate.hasKey(key2);
    value = valueOperations.get(key2);
    log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey, value);
}

使缓存中的Key失效与判断 Key是否存在,在实际业务场景中是很常用的,最常见的场景包括:

(1)将数据库查询到的数据缓存一定的时间 TTL,在 TTL时间内前端查询访问数据列表时,只需要在缓存中查询即可,从而减轻数据库的查询压力。

(2)将数据压入缓存队列中,并设置一定的TTL时间,当TTL时间一到,将触发监听事件,从而处理相应的业务逻辑。

Redis缓存穿透

如果前端频繁发起访问请求时,恶意提供数据库中不存在的Key,则此时数据库中查询到的数据将永远为Null。由于Null的数据是不存入缓存中的,因而每次访问请求时将查询数据库,如果此时有恶意攻击,发起“洪流”式的查询,则很有可能会对数据库造成极大的压力,甚至压垮数据库。这个过程称之为“缓存穿透”。

解决方案:当查询数据库时如果没有查询到数据,则将 Null返回给前端用户,同时将该Null数据塞入缓存中,并将对应的Key设置一定的过期时间TTL。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 缓存穿透实战
 **/
@RestController
public class CachePassController {

    private static final Logger log = LoggerFactory.getLogger(CachePassController.class);

    private static final String prefix = "cache/pass";

    @Autowired
    private CachePassService cachePassService;

    /**
     * 获取热销商品信息
     *
     * @param itemCode
     * @return
     */
    @RequestMapping(value = prefix + "/item/info", method = RequestMethod.GET)
    public Map<String, Object> getItem(@RequestParam String itemCode) {
        Map<String, Object> resMap = new HashMap<>();
        resMap.put("code", 0);
        resMap.put("msg", "成功");

        try {
            resMap.put("data", cachePassService.getItemInfo(itemCode));
        } catch (Exception e) {
            resMap.put("code", -1);
            resMap.put("msg", "失败" + e.getMessage());
        }
        return resMap;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * 缓存穿透service
 */
@Service
public class CachePassService {

    private static final Logger log= LoggerFactory.getLogger(CachePassService.class);

    @Autowired
    private ItemMapper itemMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    private static final String keyPrefix="item:";

    /**
     * 获取商品详情-如果缓存有,则从缓存中获取;如果没有,则从数据库查询,并将查询结果塞入缓存中
     * @param itemCode
     * @return
     * @throws Exception
     */
    public Item getItemInfo(String itemCode) throws Exception{
        Item item=null;

        final String key=keyPrefix+itemCode;
        ValueOperations valueOperations=redisTemplate.opsForValue();
        if (redisTemplate.hasKey(key)){
            log.info("---获取商品详情-缓存中存在该商品---商品编号为:{} ",itemCode);

            //从缓存中查询该商品详情
            Object res=valueOperations.get(key);
            if (res!=null && !Strings.isNullOrEmpty(res.toString())){
                item=objectMapper.readValue(res.toString(),Item.class);
            }
        }else{
            log.info("---获取商品详情-缓存中不存在该商品-从数据库中查询---商品编号为:{} ",itemCode);

            //从数据库中获取该商品详情
            item=itemMapper.selectByCode(itemCode);
            if (item!=null){
                valueOperations.set(key,objectMapper.writeValueAsString(item));
            }else{
                //过期失效时间TTL设置为30分钟-当然实际情况要根据实际业务决定
                valueOperations.set(key,"",30L, TimeUnit.MINUTES);
            }
        }
        return item;
    }
}

Redis缓存雪崩

缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。

解决方案:为这些Key设置不同的、随机的TTL,从而错开缓存中Key的失效时间点,可以在某种程度上减少数据库的查询压力。

Redis缓存击穿

缓存击穿:指缓存中某个频繁被访问的Key(“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。

解决方案:应该设置这个热点Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。

不管是缓存穿透、缓存雪崩还是缓存击穿,其实它们最终导致的后果几乎都是一样的,即给DB(数据库)造成压力,甚至压垮数据库。而它们的解决方案也都有一个共性,那就是“加强防线”,尽量让高并发的读请求落在缓存中,从而避免直接跟数据库打交道。

Redis应用实战之抢红包系统

消息中间件RabbitMQ

死信队列/延迟队列

分布式锁

Redisson