《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 值:

#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 配置

这里的代码在上一节已经说过了,因为我们是开发环境,使用单机版即可。

<?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 接口和其单机/集群实现类代码省略,需要请看上一节。

@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) 即可。

@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 也就删掉了):

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

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

再次查看 Redis:

评论