Redis在项目中实战经验
首先对于项目中Redis做缓存的一些思路,避免走一些弯路
1、对于会话缓存、全页缓存(FPC)的三种情况(单条缓存,对于一些不分页、不需要实时的列表,不需要实时的,需要分页的列表)
(1)单条数据:表名+id作为key永久保存到redis,在更新的地方都要更新缓存
缺点:不适用与需要经常更新的数据;
(2)不分页:我们可以将列表结果缓存到redis中,设定一定的缓存过期时间,获取该列表的方法名作为key,列表结果为value;
缺点:这种情况只适用于不经常更新且不需要实时的情况下;
(3)分页:分页列表,可以把分页结果列表放到一个map,然后将该map存到reids的list中,然后给list设置一个缓存存活时间(expire),这样通过lrange出来就能获取存有分页列表的数据,遍历该list,通过遍历list中map的key判断该分页数据是否在缓存内,是则返回,不存在rpush进去;
缺点:这种做法能解决1-5页的数据已经重新加载,而6-10页的数据依然是缓存的数据而导致脏数据的情况
适用场景1
什么样的情况才会用到缓存呢??一个项目中有些数据长时间不会发生变动,但是用户又访问特别频繁。我觉得这样的情况会用到缓存。从我们项目的使用情况,我总结出来了这一点。
我们项目的首页上会有一些大的广告位,而且大家都知道用户最先访问的都会是首页,所以访问特别频繁,广告更换也不会很频繁,所以我们觉得用在这里挺合适的。
如何让数据从缓存中取到
第一,我们首先会判断缓存中是否有该数据,如果没有就从数据库中获取,返回给前台的同时存入到缓存一份儿。如果缓存中有数据,则直接返回缓存中的数据。(只截取一段儿代码,主要给大家讲解思路)
[java] view plain copy
@Override
public List getContentList(Long cid) {
// 查询数据时,先从缓存查询,有就直接返回
try {
String json = jedisClient.hget(REDIS_CONTENT_KEY, cid + "");
if (!StringUtils.isEmpty(json)) {
// 把json数据转换成List
List jsonToList = JsonUtils.jsonToList(json,
TbContent.class);
return jsonToList;
}
} catch (Exception e1) {
e1.printStackTrace();
}
// 如果没有则查询数据库
// 根据cid查询内容列表
TbContentExample example = new TbContentExample();
Criteria criteria = example.createCriteria();
criteria.andCategoryIdEqualTo(cid);
// 执行查询
List list = contentMapper.selectByExampleWithBLOBs(example);
// 查询之后再放入缓存
// 有关content的保存在一个hash中,key为REDIS_CONTENT_KEY
// 其中每项的item为contentCid,value为list(list要转换成json)
try {
jedisClient.hset(REDIS_CONTENT_KEY, cid + "",
JsonUtils.objectToJson(list));
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
如何让缓存中的数据与数据库中的保持同步
刚开始接触的时候,我有一个疑问,不知道大家有没有,就是在项目中如果使用redis做缓存的话,如果数据有变化的时候怎么让redis中的数据与数据库中的保持同步。应该会有很多种方法,我们项目中用的是下面这种方法。
在后台对大广告位的内容进行添加或修改或删除的同时删除缓存中大广告位的信息。
[java] view plain copy
/**
* 添加内容
* @param content
* @return
*/
@RequestMapping("/save")
@ResponseBody
public TaotaoResult insertContent(TbContent content){
TaotaoResult result = null;
try {
//添加大广告位信息
result = contentService.insertContent(content);
//删除缓存中的信息
jedisClient.hde(REST_BASE_URL+REST_CONTENT_SYNC_URL+content.getCategoryId());
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
2、队列
适用场景2
聊天内容进行存储,考虑到数据库查询的io连接数高、连接频繁的因素,决定利用缓存做;
redis可以对所有二进制的存储,java可以对所有对象进行序列化的,
你需要先测试一下环境是否可以运行,端口是否可以,保证这些是前提条件
JedisPoolConnect.java
package com.zhijin.coding.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* Created by User on 2017/11/18.
*/
public class JedisPoolConnect {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("config.xml");
JedisPool jedisPool = (JedisPool) context.getBean("jedisPool");
Jedis client = jedisPool.getResource();
// client.select(0);
client.set("dddd","ssss");
System.out.println(client.get("dddd"));
jedisPool.returnBrokenResource(client);
}
}
config.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="32">property>
<property name="maxIdle" value="6">property>
<property name="maxWaitMillis" value="15000">property>
<property name="minEvictableIdleTimeMillis" value="300000">property>
<property name="numTestsPerEvictionRun" value="3">property>
<property name="timeBetweenEvictionRunsMillis" value="60000">property>
<property name="blockWhenExhausted" value="1">property>
bean>
<bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy">
<constructor-arg ref="jedisPoolConfig">constructor-arg>
<constructor-arg value="127.0.0.1">constructor-arg>
<constructor-arg value="6379">constructor-arg>
<constructor-arg value="150000">constructor-arg>
bean>
beans>
如果这些是可以的,那么进行下一步
Message.java
package com.zhijin.coding.test;
/**
* Created by User on 2017/11/18.
*/
import java.io.Serializable;
/**定义消息类接收消息内容和设置消息的下标
* @author lenovo
*
*/
public class Message implements Serializable{
private static final long serialVersionUID = 7792729L;
private int id;
private String content;
public Message(int id, String content) {
this.id = id;
this.content = content;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Configuration.java
package com.zhijin.coding.test;
/**
* Created by User on 2017/11/18.
*/
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* Created by Kinglf on 2016/10/17.
*/
public class Configuration extends Properties {
private static final long serialVersionUID = -2296275030489943706L;
private static Configuration instance = null;
public static synchronized Configuration getInstance() {
if (instance == null) {
instance = new Configuration();
}
return instance;
}
public String getProperty(String key, String defaultValue) {
String val = getProperty(key);
return (val == null || val.isEmpty()) ? defaultValue : val;
}
public String getString(String name, String defaultValue) {
return this.getProperty(name, defaultValue);
}
public int getInt(String name, int defaultValue) {
String val = this.getProperty(name);
return (val == null || val.isEmpty()) ? defaultValue : Integer.parseInt(val);
}
public long getLong(String name, long defaultValue) {
String val = this.getProperty(name);
return (val == null || val.isEmpty()) ? defaultValue : Integer.parseInt(val);
}
public float getFloat(String name, float defaultValue) {
String val = this.getProperty(name);
return (val == null || val.isEmpty()) ? defaultValue : Float.parseFloat(val);
}
public double getDouble(String name, double defaultValue) {
String val = this.getProperty(name);
return (val == null || val.isEmpty()) ? defaultValue : Double.parseDouble(val);
}
public byte getByte(String name, byte defaultValue) {
String val = this.getProperty(name);
return (val == null || val.isEmpty()) ? defaultValue : Byte.parseByte(val);
}
public Configuration() {
InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream("config.xml");
try {
this.loadFromXML(in);
in.close();
} catch (IOException ioe) {
}
}
}
JedisUtils.java //封装的方法
package com.zhijin.coding.test;
/**
* Created by User on 2017/11/18.
*/
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JedisUtils {
private static String JEDIS_IP;
private static int JEDIS_PORT;
private static String JEDIS_PASSWORD;
//private static String JEDIS_SLAVE;
private static JedisPool jedisPool;
static {
Configuration conf = Configuration.getInstance();
JEDIS_IP = conf.getString("jedis.ip", "127.0.0.1");
JEDIS_PORT = conf.getInt("jedis.port", 6379);
JEDIS_PASSWORD = conf.getString("jedis.password", null);
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(5000);
config.setMaxIdle(256);//20
config.setMaxWaitMillis(5000L);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
config.setTestWhileIdle(true);
config.setMinEvictableIdleTimeMillis(60000l);
config.setTimeBetweenEvictionRunsMillis(3000l);
config.setNumTestsPerEvictionRun(-1);
jedisPool = new JedisPool(config, JEDIS_IP, JEDIS_PORT, 60000);
}
/**
* 获取数据
* @param key
* @return
*/
public static String get(String key) {
String value = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
value = jedis.get(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return value;
}
public static void close(Jedis jedis) {
try {
jedisPool.returnResource(jedis);
} catch (Exception e) {
if (jedis.isConnected()) {
jedis.quit();
jedis.disconnect();
}
}
}
/**
* 获取数据
*
* @param key
* @return
*/
public static byte[] get(byte[] key) {
byte[] value = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
value = jedis.get(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return value;
}
public static void set(byte[] key, byte[] value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key, value);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static void set(byte[] key, byte[] value, int time) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key, value);
jedis.expire(key, time);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static void hset(byte[] key, byte[] field, byte[] value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hset(key, field, value);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static void hset(String key, String field, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hset(key, field, value);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
/**
* 获取数据
*
* @param key
* @return
*/
public static String hget(String key, String field) {
String value = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
value = jedis.hget(key, field);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return value;
}
/**
* 获取数据
*
* @param key
* @return
*/
public static byte[] hget(byte[] key, byte[] field) {
byte[] value = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
value = jedis.hget(key, field);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return value;
}
public static void hdel(byte[] key, byte[] field) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hdel(key, field);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
/**
* 存储REDIS队列 顺序存储
* @param byte[] key reids键名
* @param byte[] value 键值
*/
public static void lpush(byte[] key, byte[] value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.lpush(key, value);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
/**
* 存储REDIS队列 反向存储
* @param byte[] key reids键名
* @param byte[] value 键值
*/
public static void rpush(byte[] key, byte[] value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.rpush(key, value);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
/**
* 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端
* @param byte[] key reids键名
* @param byte[] value 键值
*/
public static void rpoplpush(byte[] key, byte[] destination) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.rpoplpush(key, destination);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
/**
* 获取队列数据
* @param byte[] key 键名
* @return
*/
public static List<byte[]> lpopList(byte[] key) {
List<byte[]> list = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
list = jedis.lrange(key, 0, -1);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return list;
}
/**
* 获取队列数据
* @param byte[] key 键名
* @return
*/
public static byte[] rpop(byte[] key) {
byte[] bytes = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
bytes = jedis.rpop(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return bytes;
}
public static void hmset(Object key, Map hash) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hmset(key.toString(), hash);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static void hmset(Object key, Map hash, int time) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hmset(key.toString(), hash);
jedis.expire(key.toString(), time);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static List hmget(Object key, String... fields) {
List result = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.hmget(key.toString(), fields);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return result;
}
public static Set hkeys(String key) {
Set result = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.hkeys(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return result;
}
public static List<byte[]> lrange(byte[] key, int from, int to) {
List<byte[]> result = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.lrange(key, from, to);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return result;
}
public static Map<byte[], byte[]> hgetAll(byte[] key) {
Map<byte[], byte[]> result = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.hgetAll(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return result;
}
public static void del(byte[] key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
}
public static long llen(byte[] key) {
long len = 0;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.llen(key);
} catch (Exception e) {
//释放redis对象
jedisPool.returnBrokenResource(jedis);
e.printStackTrace();
} finally {
//返还到连接池
close(jedis);
}
return len;
}
}
RedisQueneTest.java//测试方法
package com.zhijin.coding.test;
/**
* Created by User on 2017/11/18.
*/
public class RedisQueneTest {
public static void main(String[] args) {
pop();
}
private static void pop() {
byte[] bytes = JedisUtils.rpop(redisKey);
Message msg = null;
try {
msg = (Message) Utils.bytesToObject(bytes);
} catch (Exception e) {
e.printStackTrace();
}
if (msg != null) {
System.out.println(msg.getId() + " " +msg.getContent());
}
}
public static byte[] redisKey = "key".getBytes();
static {
try {
init();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void init() throws Exception {
Message m1 = new Message(1,"消息推送——发布1");
System.out.println(redisKey.toString());
JedisUtils.lpush(redisKey,Utils.objectToBytes(m1));
Message m2 = new Message(2,"消息推送——发布2");
JedisUtils.lpush(redisKey,Utils.objectToBytes(m2));
}
}
他们之间都是耦合关系,如果出现错误,你要自行解决,这样才能转化为你自己的东西
tips:加载config.xml可能会出现路径错误
3、排行榜/计数器
使用场景3
用redis实现计数器
社交产品业务里有很多统计计数的功能,比如:
用户: 总点赞数,关注数,粉丝数
帖子: 点赞数,评论数,热度
消息: 已读,未读,红点消息数
话题: 阅读数,帖子数,收藏数
统计计数的特点
实时性要求高
写的频率很高
写的性能对MySQL是一个挑战
可以采用redis来优化高频率写入的性能要求。
redis优化方案一
对于每一个实体的计数,设计一个hash结构的counter:
//用户counter:user:{userID}
-> praiseCnt: 100 //点赞数
-> hostCnt: 200 //热度
-> followCnt: 332 //关注数
-> fansCnt: 123 //粉丝数
//帖子counter:topic:{topicID}
-> praiseCnt: 100 //点赞数
-> commentCnt: 322 //评论数
//话题counter:subject:{subjectID}
-> favoCnt: 312 //收藏数
-> viewCnt: 321 //阅读数
-> searchCnt: 212 //搜索进入次数
-> topicCnt: 312 //话题中帖子数
类似这种计数器,随着产品功能的增加,也会越来越多,比如回复数,踩数,转发数什么的。
redis相关的命令
//获取指定userID的所有计数器
HGETALL counter:user:{userID}
//获取指定userID的指定计数器
HMGET counter:user:{userID} praiseCnt hostCnt
//指定userID点赞数+1
HINCRBY counter:user:{userID} praiseCnt
缺点:
这样设计,如果要批量查询多个用户的数据,就比较麻烦,例如一次要查指定20个userID的计数器?只能循环执行 HGETALL counter:user:{userID}。
优点:
以实体聚合数据,方便数据管理
redis优化方案二
方案二是用来解决方案一的缺点的,依然是采用hash,结构设计是这样的:
counter:user:praiseCnt
-> userID_1001: 100
-> userID_1002: 200
-> userID_1003: 332
-> userID_1004: 123
…….
-> userID_9999: 213
counter:user:hostCnt
-> userID_1001: 10
-> userID_1002: 290
-> userID_1003: 322
-> userID_1004: 143
…….
-> userID_9999: 213
counter:user:followCnt
-> userID_1001: 21
-> userID_1002: 10
-> userID_1003: 32
-> userID_1004: 203
…….
-> userID_9999: 130
获取多个指定userID的点赞数的命令变成这样了:
HMGET counter:user:praiseCnt userID_1001 userID_1002
上面命令可以批量获取多个用户的点赞数,时间复杂度为O(n),n为指定userID的数量。
优点:
解决了批量操作的问题
缺点:
当要获取多个计数器,比如同时需要praiseCnt,hostCnt时,要读多次,不过要比第一种方案读的次数要少。
一个hash里的字段将会非常宠大,HMGET也许会有性能瓶颈。
用redis管道(Pipelining)来优化方案一
对于第一种方案的缺点,可以通过redis管道来优化,一次性发送多个命令给redis执行:
$userIDArray = array(1001, 1002, 1003, 1009);
pipe= redis->multi(Redis::PIPELINE);
foreach ( userIDArrayas userID) {
pipe−>hGetAll(′counter:user:′. userID);
}
replies= pipe->exec();
print_r($replies);
4、发布/订阅
发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!
PublishRedis.java
package com.zhijin.coding.controller;
import redis.clients.jedis.Jedis;
/**
* Created by User on 2017/11/17.
*/
public class PublishRedis {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost",6379);
jedis.publish("ch1","hello redis");
jedis.close();
}
}
SubscribeRedis.java
package com.zhijin.coding.controller;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
/**
* Created by User on 2017/11/17.
*/
public class SubscribeRedis extends JedisPubSub{
Jedis jedis = new Jedis("127.0.0.1",6379);
JedisPubSub jedisPubSub = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("收到消息" + message);
if("unsubscribe".equals(message)) {
this.unsubscribe();
}
}
@Override
public void onPMessage(String pattern, String channel, String message) {
super.onPMessage(pattern, channel, message);
}
@Override
public void onPSubscribe(String pattern, int subscribedChannels) {
super.onPSubscribe(pattern, subscribedChannels);
}
@Override
public void onPUnsubscribe(String pattern, int subscribedChannels) {
super.onPUnsubscribe(pattern, subscribedChannels);
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
super.onSubscribe(channel, subscribedChannels);
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
super.onUnsubscribe(channel, subscribedChannels);
}
};
public static void main(String[] args) {
Jedis jedis = null;
try {
jedis = new Jedis("127.0.0.1",6379);
SubscribeRedis sr = new SubscribeRedis();
sr.onPUnsubscribe("ch1", 1);
sr.subscribe();
}catch (Exception e){
e.printStackTrace();
}finally {
if(jedis!=null){
jedis.disconnect();
}
}
}
}
tips:必须要开启redis本地服务才能运行啊!
参考文章:https://www.cnblogs.com/JockChou/p/4647973.html