《Redis初探(7)——Jedis操纵集群》中,我们已经学会了搭建 Redis 集群,以及使用策略模式,在xml文件中灵活切换单机版和集群版。

本章将演示在宜立方商城项目中使用 Redis,项目地址:e3mall

一、功能需求

商城首页访问量巨大,因为首页的大轮播图是从数据库查询获取的,每次访问都要查询一次数据库,数据库压力巨大,亟需缓存。

二、功能实现

实现之前首先思考 Redis 是要加在 Service 层还是 Web 层。理论上来说都可以,但是加在 Web 层的话,其他 Web 去调用 Service 还是得去查数据库,因此我们加在 Service 层。

其次思考使用什么数据类型,我们使用哈希类型,field 为类别的 id,value 为对应查询的查询内容。

2.1 配置文件 cfg.properties

首先在配置文件中加入 Redis 相关的信息,最后一项 redis.CONTENT_KEY 为我们首页轮播图缓存的 key 值:

cfg.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#Redis单机
redis.standalone.host=192.168.30.155
redis.standalone.port=6379

#Redis集群
redis.cluster.01.host=192.168.30.155
redis.cluster.01.port=7001
redis.cluster.02.host=192.168.30.155
redis.cluster.02.port=7002
redis.cluster.03.host=192.168.30.155
redis.cluster.03.port=7003
redis.cluster.04.host=192.168.30.155
redis.cluster.04.port=7004
redis.cluster.05.host=192.168.30.155
redis.cluster.05.port=7005
redis.cluster.06.host=192.168.30.155
redis.cluster.06.port=7006

#Redis key相关
#用于存放tb_content表的缓存(哈希类型)
redis.CONTENT_KEY=CONTENT_KEY

2.2 Spring 中 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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 注意:单机和集群同时只能放开一个 -->

<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:cfg.properties"/>

<!-- 配置Redis单机 -->
<bean id="jedisClientPool" class="jit.wxs.common.jedis.JedisClientPool">
<property name="jedisPool" ref="jedisPool"/>
</bean>
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="${redis.standalone.host}"/>
<constructor-arg name="port" value="${redis.standalone.port}"/>
</bean>

<!-- 配置Redis集群 -->
<!--<bean id="jedisClientCluster" class="jit.wxs.common.jedis.JedisClientCluster">-->
<!--<property name="jedisCluster" ref="jedisCluster"/>-->
<!--</bean>-->
<!--<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">-->
<!--<constructor-arg>-->
<!--<set>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.01.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.01.port}"/>-->
<!--</bean>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.02.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.02.port}"/>-->
<!--</bean>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.03.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.03.port}"/>-->
<!--</bean>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.04.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.04.port}"/>-->
<!--</bean>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.05.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.05.port}"/>-->
<!--</bean>-->
<!--<bean class="redis.clients.jedis.HostAndPort">-->
<!--<constructor-arg name="host" value="${redis.cluster.06.host}"/>-->
<!--<constructor-arg name="port" value="${redis.cluster.06.port}"/>-->
<!--</bean>-->
<!--</set>-->
<!--</constructor-arg>-->
<!--</bean>-->
</beans>

2.3 Service 层代码

首先我们注入了 JedisClient,然后从配置文件取到了 key 的名字 CONTENT_KEY

listByCategoryId() 方法中,我们先查询 Redis 中是否有存在的 field,如果有,直接返回;如果没有,先查询数据库,然后存入缓存。

为了保证缓存的同步,在添加和删除方法中,我直接删除掉了相应 field 的缓存,这样当执行查询方法时,会重新保存缓存。

需要注意的是,Redis 的正常/异常与否,不应当影响程序的正常运行。因为即使没有 Redis 程序也是可以正常运行的,因此我们在 Redis 操作的地方,需要 try-catch,在 catch 中可以打印日志信息等操作,我这里只是简单的输出在控制台。

注:JedisClient 接口和其单机/集群实现类代码省略,需要请看上一节。

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
@Service
public class TbContentServiceImpl extends ServiceImpl<TbContentMapper, TbContent> implements TbContentService {
@Autowired
private TbContentMapper contentMapper;

@Autowired
private JedisClient jedisClient;

@Value("${redis.CONTENT_KEY}")
private String CONTENT_KEY;

/**
* 删除CONTENT_KEY中指定field
*/
private void deleteContentKeyFromRedis(Long cid) {
try {
jedisClient.hdel(CONTENT_KEY, cid + "");
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public List<TbContent> listByCategoryId(Long cid) {
try {
// 如果缓存存在的话,直接从缓存中取
String json = jedisClient.hget(CONTENT_KEY, cid + "");
if(StringUtils.isNotBlank(json)) {
return JsonUtils.jsonToList(json, TbContent.class);
}
} catch (Exception e) {
e.printStackTrace();
}

List<TbContent> contents = contentMapper.selectList(new EntityWrapper<TbContent>() .eq("category_id", cid));

try {
// 加入缓存
jedisClient.hset(CONTENT_KEY, cid+"", JsonUtils.objectToJson(contents));
} catch (Exception e) {
e.printStackTrace();
}

return contents;
}

@Override
public void addContent(TbContent tbContent) {
// 更新缓存
deleteContentKeyFromRedis(tbContent.getCategoryId());

tbContent.setCreated(new Date());
tbContent.setUpdated(new Date());

contentMapper.insert(tbContent);
}

@Override
public void deleteById(Long id) {
if(id == null) {
return;
}
// 更新缓存
TbContent tbContent = contentMapper.selectById(id);
deleteContentKeyFromRedis(tbContent.getCategoryId());

contentMapper.deleteById(id);
}
}

2.4 Web 层代码

我们设首页轮播图的 id 为 ad1Id,直接调用 tbContentService.listByCategoryId(ad1Id) 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class PageController {
@Value("${ad1.id}")
private Long ad1Id;

@Autowired
private TbContentService tbContentService;

@RequestMapping("/index")
public String showIndex(Model model) {
// 得到首页大轮播图的List
List<TbContent> ad1List = tbContentService.listByCategoryId(ad1Id);

model.addAttribute("ad1List", ad1List);
return "index";
}
}

三、验证

服务器启动单机版 Redis,当我们刷新首页的时候,就会将缓存保存到了 Redis 中。

Key 为 CONTENT_KEY,field 目前只有一个,即首页轮播图,其值为89,value 为转换为 json 的数据:

当我在后台为首页添加一个轮播图后,该 Field 被删除掉了(这里之所以连 key 也被删掉了,是因为该 key 中只有一个field,因此唯一的 field 被删掉了,key 也就删掉了):

1
2
127.0.0.1:6379> keys *
(empty list or set)

重新刷新首页,正确显示三张:

再次查看 Redis: