实践-缓存穿透、击穿

成长就是不断发现过去傻逼的过程,这里讲讲平时开发经常用到缓存。

缓存穿透和缓存击穿

  • 本内容相关代码地址:cache-breakdown
  • 代码运行需要mysql和redis环境

缓存穿透

  • 理想的情况下我们数据缓存到redis里面,客户访问数据的时候直接去缓存查询,如果没有缓存就去数据库查询。
  • 仔细想想这里其实有一个bug,必须是缓存有的数据才会去数据库查询,如果一直都是在查询一个没有得数据咋办呢,缓存会一直落空,而且每次都会去数据库查询。

常用解决思路

这里注意列举常用方案,根据实际情况使用

空值缓存

在访问缓存key之前,设置另一个短期key来锁住当前key的访问。这个方案可以降低持续缓存落空的情况。至少数据库被穿透的概率小很多了。

  • Redis方案
    • 如上图所示,当缓存和数据库中都没找到的时候就将查询条件作为key并在缓存中插入一个空的数据,并指定好过期时间。这样下次的请求就会被缓存命中。

代码实现

  • redis字符串操作接口
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

/**
* @author z201.coding@gmail.com
*/
public interface CacheBreakdownServiceI {

/**
* 写入字符串类型
*
* @param key
* @param value
* @return
*/
Boolean setString(String key, String value);

/**
* 写入字符串并设置过期时间
* @param key
* @param timeout
* @param value
* @return
*/
Boolean setExString(String key, Long timeout, String value);

/**
* 获取字符串类型
*
* @param key
* @return
*/
String getString(String key);

/**
* 设置key默认过期时间
*
* @param key
* @return
*/
Boolean expireKey(String key);

/**
* 设置key过期时间
*
* @param key
* @param timeout millis
* @return
*/
Boolean expireKey(String key, Long timeout);

/**
* SET if Not exists
* 只在键 key 不存在的情况下, 将键 key 的值设置为 value 。
* 若键 key 已经存在, 则 SETNX 命令不做任何动作。
* (如果不存在,则 SET)的简写。
* @param key
* @return
*/
Boolean setNx(String key);

/**
* 删除
* @param key
* @return
*/
Long del(String key);


}
  • 实现类
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

/**
* @author z201.coding@gmail.com
**/
@Service
@Slf4j
public class CacheBreakdownServiceImpl implements CacheBreakdownServiceI {

public static final String CACHE_NAME = "CACHE-PERSON:";

public static final long CACHE_OUT_TIME = 600; // 6秒

@Autowired
RedisTemplate redisTemplate;

@Override
public Boolean setString(String key, String value) {
return setExString(key, CACHE_OUT_TIME, value);
}

@Override
public Boolean setExString(String key, Long timeout, String value) {
notNull(key);
notNull(value);
if (null == timeout || timeout <= 0) {
timeout = CACHE_OUT_TIME;
}
final Long finalTimeout = timeout;
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
return connection.setEx(
stringRedisSerializer.serialize(CACHE_NAME + key),
finalTimeout,
stringRedisSerializer.serialize(value));
});
}

@Override
public String getString(String key) {
notNull(key);
return (String) redisTemplate.execute((RedisCallback<String>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
final byte[] bytes = connection.get(stringRedisSerializer.serialize(CACHE_NAME + key));
return stringRedisSerializer.deserialize(bytes);
});
}

@Override
public Boolean expireKey(String key) {
return expireKey(key, CACHE_OUT_TIME);
}

@Override
public Boolean expireKey(String key, Long timeout) {
notNull(key);
if (null == timeout || timeout <= 0) {
timeout = CACHE_OUT_TIME;
}
final Long finalTimeout = timeout;
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
return connection.pExpire(stringRedisSerializer.serialize(CACHE_NAME + key), finalTimeout);
});
}

@Override
public Boolean setNx(String key) {
notNull(key);
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
return connection.setNX(stringRedisSerializer.serialize(CACHE_NAME + key), stringRedisSerializer.serialize("SetNx"));
});
}

@Override
public Long del(String key) {
notNull(key);
return (Long) redisTemplate.execute((RedisCallback<Long>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
return connection.del(stringRedisSerializer.serialize(CACHE_NAME + key));
});
}

private final void notNull(String key) {
if (null == key || key.isEmpty()) {
throw new RuntimeException();
}
}
}
  • 测试数据操作接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14

/**
* @author z201.coding@gmail.com
**/
public interface PersonServiceI {

/**
* 根据主键获取用户
* @param id
* @return
*/
Person findById(Long id);

}
  • 接口实现类
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
/**
* @author z201.coding@gmail.com
**/
@Service
@Slf4j
public class PersonServiceImpl implements PersonServiceI {

public static final String INFO = "INFO:";

@Autowired
PersonDao personDao;

@Autowired
private CacheBreakdownServiceI cacheBreakdownService;

@Override
public Person findById(Long id) {
Gson gson = new GsonBuilder().create();
String key = INFO + id;
String data = cacheBreakdownService.getString(key);
if (data == null) {
log.info("get db");
Person person = personDao.findById(id);
if (person != null) {
data = gson.toJson(person);
if (cacheBreakdownService.setString(key, data)) {
log.info("set cache ");
}
}else {
return new Person();
}
}
log.info("get cache");
Person person = gson.fromJson(data,Person.class);
return person;
}
}
  • 数据准备

导入数据库person.sql,也可以自己参考测试代码生成测试数据。

1
2
3
4
5
6
# 查看数据库数据
SELECT COUNT(*) FROM `person`
>> 92022
# 找一条有的数据
SELECT full_name FROM `person` WHERE id = 1
>> 灵芸黄
  • 单元测试代码,理想状态下的缓存。
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testCacheBreakdownInfo() {
long startTimes = System.currentTimeMillis();
log.info("fullName {} ", personService.findById(1L).getFullName());
log.info("fullName {} ", personService.findById(1L).getFullName());
log.info("fullName {} ", personService.findById(1L).getFullName());
log.info("fullName {} ", personService.findById(1L).getFullName());
log.info("fullName {} ", personService.findById(1L).getFullName());
long endTimes = System.currentTimeMillis();
log.info("执行完毕: {}", (endTimes - startTimes));
}
  • 日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.013 seconds (JVM running for 2.997)
[main] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[main] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[main] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName 灵芸黄
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName 灵芸黄
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName 灵芸黄
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName 灵芸黄
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName 灵芸黄
[main] INFO c.z201.cache.main.CacheBreakdownTest - 执行完毕: 685

这里可以看到第一次查询到结果被缓存,所以后面的都是查询到该缓存。

  • 演示缓存击穿的情况,id = 1000000实际上并不存在
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testCacheBreakdownInfo() {
long startTimes = System.currentTimeMillis();
log.info("fullName {} ", personService.findById(100000L).getFullName());
log.info("fullName {} ", personService.findById(100000L).getFullName());
log.info("fullName {} ", personService.findById(100000L).getFullName());
log.info("fullName {} ", personService.findById(100000L).getFullName());
log.info("fullName {} ", personService.findById(100000L).getFullName());
long endTimes = System.currentTimeMillis();
log.info("执行完毕: {}", (endTimes - startTimes));
}
  • 日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.009 seconds (JVM running for 2.973)
[main] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[main] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z201.cache.main.CacheBreakdownTest - 执行完毕: 658

这里可以看到成功的缓存击穿,因为缓存未命中,数据库里面也没有。

空值缓存方法实施

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
@Override
public synchronized Person findById(Long id) {
Gson gson = new GsonBuilder().create();
String key = INFO + id;
String data = cacheBreakdownService.getString(key);
if (data == null) {
log.info("get db");
Person person = personDao.findById(id);
if (person != null) {
data = gson.toJson(person);
if (cacheBreakdownService.setString(key, data)) {
log.info("set cache ");
}
} else {
// 插入一个空格进去
if (cacheBreakdownService.setString(key, " ")) {
log.info("set cache value null");
}
return new Person();
}
}
if (data == null || data.isEmpty() || data.trim().length() == 0) {
return new Person();
}
log.info("get cache");
Person person = gson.fromJson(data, Person.class);
return person;
}
  • 日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 1.965 seconds (JVM running for 2.973)
[main] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[main] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[main] INFO c.z.c.service.impl.PersonServiceImpl - get db
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[main] INFO c.z.c.service.impl.PersonServiceImpl - set cache value null # 这里空值缓存
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[main] INFO c.z201.cache.main.CacheBreakdownTest - 执行完毕: 662
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

使用空值缓存,同样的请求条件key在第一次被缓存穿透后,就被过期缓存了,所以在过期范围内就会命中缓存。

缓存击穿

这个场景更多是应用在多线程的环境,这里衔接上面的代码,当并发量上来的时候,大量的查询集中查询某一个key,若这个时候key刚好失效了,就会导致请求全部进入数据中。这个就是所谓的缓存击穿。

  • 演示代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testCacheBreakdownInfo() {
long startTimes = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
personService.findById(1L);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTimes = System.currentTimeMillis();
log.info("执行完毕: {}", (endTimes - startTimes));
}
  • 日志输出
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
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.139 seconds (JVM running for 3.156)
[Thread-6] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[Thread-6] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-5] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-12] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-8] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-13] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-10] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-9] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-11] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-10] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[Thread-10] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-9] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-12] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-11] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-5] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-8] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-13] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-10] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - 执行完毕: 738
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

并发起来,更新操作所有的请求都进入。

常用解决思路

这里注意列举常用方案,根据实际情况使用

Redis SETNX方案

  • 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。

代码实现

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
@Override
public Person findById(Long id) {
Gson gson = new GsonBuilder().create();
String key = INFO + id;
String data = cacheBreakdownService.getString(key);
if (data == null) {
// 设置 setNx
final String nxKey = NX + id;
if (cacheBreakdownService.setNx(nxKey)) {
// 指定过期时间 1 秒
cacheBreakdownService.expireKey(nxKey, 1L);
log.info("get db ");
Person person = personDao.findById(id);
if (person != null) {
data = gson.toJson(person);
if (cacheBreakdownService.setString(key, data)) {
log.info("set cache ");
cacheBreakdownService.del(nxKey);
return person;
}
} else {
// 插入一个空格进去
if (cacheBreakdownService.setString(key, " ")) {
log.info("set cache value null");
}
return new Person();
}
}
}
if (data == null || data.isEmpty() || data.trim().length() == 0) {
log.info("get null");
return new Person();
}
log.info("get cache");
Person person = gson.fromJson(data, Person.class);
return person;
}
  • 再次运行测试代码
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
@Test
public void testCacheBreakdownInfo() {
long startTimes = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
log.info(" fullName {} " , personService.findById(2L).getFullName());;
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
CountDownLatch countDownLatch2 = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
log.info(" fullName {} " , personService.findById(3L).getFullName());;
countDownLatch2.countDown();
}).start();
}
try {
countDownLatch2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTimes = System.currentTimeMillis();
log.info("执行完毕: {}", (endTimes - startTimes));
}

这里为了避免过于集中,所以分成两组线程,第一组当中任意线程成功将缓存更新,第二组线程就能获取到新的缓存。

  • 日志输出
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
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.029 seconds (JVM running for 3.134)
[Thread-7] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[Thread-7] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[Thread-8] INFO c.z.c.service.impl.PersonServiceImpl - get null
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - get null
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - get null
[Thread-8] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[Thread-4] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[Thread-6] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[Thread-5] INFO c.z.c.service.impl.PersonServiceImpl - get null
[Thread-5] INFO c.z201.cache.main.CacheBreakdownTest - fullName null
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-7] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[Thread-7] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-7] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[Thread-10] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-9] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-11] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-12] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-13] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-10] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[Thread-11] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[Thread-12] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[Thread-9] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[Thread-13] INFO c.z201.cache.main.CacheBreakdownTest - fullName 碧琪刘
[main] INFO c.z201.cache.main.CacheBreakdownTest - 执行完毕: 697
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

第一组线程中任意一个成功更新的缓存,第二组就可以直接走缓存。

分布式锁改进

  • 上面的setnxpExpire是分开的,这里其实会出现一个问题就是当setnx设置成功但是pExpire执行的时候突然程序中断了怎么办。这样就编程了死锁。因此才有了改进的方案使用set 的扩张来同时实现 nx ex。

  • 添加接口

1
2
3
4
5
6
7
8
9
/**
* set扩展方式
* @param key
* @param value
* @param timeout
* @param option
* @return
*/
Boolean set(String key, String value, Long timeout, RedisStringCommands.SetOption option);
  • 接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Boolean set(String key, String value,Long timeout, RedisStringCommands.SetOption option) {
notNull(key);
notNull(value);
if (null == timeout || timeout <= 0) {
timeout = CACHE_OUT_TIME;
}
final Expiration expiration = Expiration.from(timeout, TimeUnit.SECONDS);
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
return connection.set(
stringRedisSerializer.serialize(CACHE_NAME + key),
stringRedisSerializer.serialize(value),
expiration,
option);
});
}

这里就不演示代码测试效果了。

Java 锁方案

如果就单纯的使用一把锁能否解决问题呢。

代码实现

  • 接口
1
2
3
4
5
6
7
/**
* 根据限制查询
* @param offset 偏移量
* @param length 获取数量
* @return
*/
List<Person> listByLimit(Integer offset , Integer length);
  • 实现类
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
@Override
public synchronized List<Person> listByLimit(Integer offset, Integer length) {
if (null == offset || offset < 0) {
offset = 0;
}
if (null == length || length <= 0) {
length = 10;
}
Gson gson = new GsonBuilder().create();
String data = cacheBreakdownService.getString(LIST);
if (data == null) {
log.info("get db");
List<Person> list = personDao.listByLimit(offset, length);
if (list != null) {
data = gson.toJson(list);
if (cacheBreakdownService.setString(LIST, data)) {
log.info("set cache ");
}
return list;
}else{
// 插入一个空格进去
if (cacheBreakdownService.setString(LIST, " ")) {
log.info("set cache value null");
}
return new ArrayList<Person>();
}
}
if (data == null || data.isEmpty() || data.trim().length() == 0) {
log.info("get null");
return new ArrayList<Person>();
}
Type type = new TypeToken<List<Person>>() {
}.getType();
log.info("get cache");
final List<Person> people = gson.fromJson(data, type);
return people;
}

这里先用重量级锁,逐步改造。

  • 测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testCacheBreakdownList() {
long startTimes = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
personService.listByLimit(0, 10);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTimes = System.currentTimeMillis();
log.info("所有线程执行完毕: {}", (endTimes - startTimes));
}
  • 日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.21 seconds (JVM running for 3.311)
[Thread-4] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[Thread-4] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-4] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[Thread-4] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-13] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-12] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-11] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-10] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-9] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-8] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-5] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - 所有线程执行完毕: 715
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

Ok,看日志确实锁了而且是悲观锁,但是这里有另外一个问题,如果更新操作超时其它的线程就会一直等待。这不是我想要的结果。

  • 改进代码
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
public volatile ReentrantLock lock = new ReentrantLock();
@Override
public List<Person> listByLimit(Integer offset, Integer length) {
if (null == offset || offset < 0) {
offset = 0;
}
if (null == length || length <= 0) {
length = 10;
}
Gson gson = new GsonBuilder().create();
String data = cacheBreakdownService.getString(LIST);
if (data == null) {
try {
lock.lockInterruptibly();
data = cacheBreakdownService.getString(LIST);
if (data == null) {
log.info("get db");
List<Person> list = personDao.listByLimit(offset, length);
if (list != null) {
data = gson.toJson(list);
if (cacheBreakdownService.setString(LIST, data)) {
log.info("set cache ");
}
return list;
} else {
// 插入一个空格进去
if (cacheBreakdownService.setString(LIST, " ")) {
log.info("set cache value null");
}
return new ArrayList<Person>();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
if (data == null || data.isEmpty() || data.trim().length() == 0) {
log.info("get null");
return new ArrayList<Person>();
}
Type type = new TypeToken<List<Person>>() {
}.getType();
log.info("get cache");
final List<Person> people = gson.fromJson(data, type);
return people;
}

第一次从缓存中检查,如果没有命中。加锁,在检查一次。如果还是找不到就从数据库中尝试更新。

  • 日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[main] INFO  c.z201.cache.main.CacheBreakdownTest - Started CacheBreakdownTest in 2.237 seconds (JVM running for 3.485)
[Thread-13] INFO io.lettuce.core.EpollProvider - Starting without optional epoll library
[Thread-13] INFO io.lettuce.core.KqueueProvider - Starting without optional kqueue library
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - get db
[Thread-6] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[Thread-6] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[Thread-6] INFO c.z.c.service.impl.PersonServiceImpl - set cache
[Thread-4] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-7] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-12] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-13] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-5] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-8] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-11] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-10] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[Thread-9] INFO c.z.c.service.impl.PersonServiceImpl - get cache
[main] INFO c.z201.cache.main.CacheBreakdownTest - 所有线程执行完毕: 773
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
[Thread-3] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

END