仿牛客网
内容部分引用至 https://blog.csdn.net/weixin_44406146
仿牛客网第四章
一、Redis入门
- Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:
字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。 - Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。
同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。 - Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。
各个数据类型应用场景:
类型 | 简介 | 特性 | 场景 |
---|---|---|---|
String(字符串) | 二进制安全 | 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M | — |
Hash(字典) | 键值对集合,即编程语言中的Map类型 | 适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) | 存储、读取、修改用户属性 |
List(列表) | 链表(双向链表) | 增删快,提供了操作某一段元素的API | 1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列 |
Set(集合) | 哈希表实现,元素不重复 | 1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 | 1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐 |
Sorted Set(有序集合) | 将Set中的元素增加一个权重参数score,元素按score有序排列 | 数据插入集合时,已经进行天然排序 | 1、排行榜 2、带权重的消息队列 |
Redis使用演示
1.reids默认有16个库,从0-15,可以使用下边的语句切换
select [index]
2.刷新数据库FLUSHDB命令,如下
3.String类型的演示
set key value [EX seconds] [PX milliseconds] [NX|XX]
get key
incr key
decr key
4.hash类型数据
hset key field value
hget key field
5.list类型数据
lpush key value [value ...]
llen key
lindex key index
lrange key start stop
lpop key
rpush key value [value ...]
rpop key
6.set类型数据
sadd key member [member ...]
scard key //统计集合中有多少元素,输出个数
spop key [count] //随机从集合中弹出数据
smembers key //相比于scard来说,输出set中所有的member
7.sortset类型数据
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zcard key
zscore key member
zrank key member //返回排名
zrange key start stop [WITHSCORES]
8.常用命令
keys *
type key
exists key [key ...]
del key
expire key seconds
二、 Spring整合Redis
-
引入依赖
-
- spring-boot-starter-data-redis
-
配置Redis
-
- 配置数据库参数
- 编写配置类,构造RedisTemplate
-
访问Redis
-
- redisTemplate.opsForValue() String
- redisTemplate.opsForHash() Hash
- redisTemplate.opsForList() List
- redisTemplate.opsForSet() Set
- redisTemplate.opsForZSet() Zset
导包
日常操作你懂得
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
配置Redis
application.properties中
- database=11表示使用第12个库
因为RedisTemplate是<String,Object>类型的不利于我们操作所以要写一个配置类
RedisConfig 配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
//设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
//设置hash的key序列化方式
template.setHashKeySerializer(RedisSerializer.string());
//设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
RedisTemplate访问redis
在Redis事务执行过程中不要进行查询,否则会返回空数据!
1.写一个测试类
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testStrings() {
String redisKey = "test:count";
redisTemplate.opsForValue().set(redisKey, 1);
System.out.println(redisTemplate.opsForValue().get(redisKey));
System.out.println(redisTemplate.opsForValue().increment(redisKey));
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}
@Test
public void testHashes() {
String redisKey = "test:user";
redisTemplate.opsForHash().put(redisKey, "id", 1);
redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");
System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
}
@Test
public void testLists() {
String redisKey = "test:ids";
redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);
System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println(redisTemplate.opsForList().index(redisKey, 0));
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}
@Test
public void testSets() {
String redisKey = "test:teachers";
redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");
System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
@Test
public void testSortedSets() {
String redisKey = "test:students";
redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
@Test
public void testKeys() {
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}
// 多次访问同一个key
@Test
public void testBoundOperations() {
String redisKey = "test:count";
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
operations.increment();
operations.increment();
operations.increment();
operations.increment();
System.out.println(operations.get());
}
//声明式事务作用于整个方法,方法中需要查询时就不合适了,所以演示编程式事务
// 编程式事务
@Test
public void testTransactional() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String redisKey = "test:tx";
operations.multi();
operations.opsForSet().add(redisKey, "zhangsan");
operations.opsForSet().add(redisKey, "lisi");
operations.opsForSet().add(redisKey, "wangwu");
//显示为[],所以一定不要在redis事务中做查询。
System.out.println(operations.opsForSet().members(redisKey));
return operations.exec();
}
});
//这里会显示所有的操作结果
System.out.println(obj);
}
}
三、点赞
-
点赞
-
- 支持对帖子、评论点赞。
- 第1次点赞,第2次取消点赞。
-
首页点赞数量
-
- 统计帖子的点赞数量。
-
详情页点赞数量
-
- 统计点赞数量。
- 显示点赞状态。
个人想法
因为我们是通过redis进行点赞的功能上线,所以我们在这一次可以不使用dao层来进行功能的书写。
Redis生成key的工具类
public class RedisUtil {
private static final String SPLIT=":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
//某个实体的赞
//like:entity:entityType:entityId 由方法参数传来的参数组成的key值
//value是个set:存的是用户id,为了统计谁给我点了赞
public static String getEntityLikeKey(int entityType,int entityId){
return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;
}
}
业务层
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
//点赞
public void like(int userId,int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
//第一次点是点赞,第二次点是取消赞
//先判断是否点过赞 value是set集合存的是userId
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
}
}
//查询实体点赞的数量
public long findEntityLikeCount(int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
//查询某人对某实体的点赞状态
//返回int 是为了以后业务扩展 比如点了踩啥的记录状态
public int findEntityLikeStatus(int userId,int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey,userId)?1:0;
}
}
表现层:点赞功能的实现
@Controller
public class LikeController {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/like",method = RequestMethod.POST)
@ResponseBody
public String like(int entityType,int entityId){
User user = hostHolder.getUser();
//实现点赞
likeService.like(user.getId(),entityType,entityId);
//统计数量
long likeCount = likeService.findEntityLikeCount(entityType,entityId);
//状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(),entityType,entityId);
Map<String,Object> map = new HashMap<>();
map.put("likeCount",likeCount);
map.put("likeStatus",likeStatus);
return CommunityUtil.getJsonString(0,null,map);
}
}
处理页面
1.帖子点赞
-
href的东西表名跳转的地方是找js的方法
-
onclick里的like方法有三个参数
-
- this用来标志是哪里点的赞(帖子可以点赞,用户评论也可以点赞)
- 1表明是给帖子点赞
- post.id是帖子的id号
-
为了方便显示数据把 赞 这个字用< b >标签包围,11用< i >包围
2.评论点赞
- 2表明给帖子评论点赞,其他同理
3.评论的回复点赞
- 3表示给评论回复点赞
4.处理js
- 新建一个discuss.js
function like(btn,entityType,entityId) {
$.post(
CONTEXT_PATH+"/like",
{"entityType":entityType,"entityId":entityId},
function (data) {
/*转化成json*/
data = $.parseJSON(data);
if(data.code==0){
$(btn).children("i").text(data.likeCount);
$(btn).children("b").text(data.likeStatus==1?"已赞":"赞");
}else{
alert(data.msg);
}
}
);
}
表现层:点赞数量的显示
1.首页上点赞数量的显示
HomeController中getIndexPage方法
处理index.html
2.帖子详情·页面上点赞数量的显示
页面处理
四、我收到的赞
-
重构点赞功能
-
- 以用户为key,记录点赞数量
- increment(key),decrement(key)
-
开发个人主页
-
- 以用户为key,查询点赞数量
重构点赞功能
1.在RedisUtil中加一个key
2.LikeService增加一个操作记录用户获得点赞的数量
并用编程式事务完成。想想为啥不用声明式事务—代码中有答案
//点赞
public void like(int userId,int entityType,int entityId,int entityUserId){ //entityUserId 就是被点赞的user的Id
/* String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
//第一次点是点赞,第二次点是取消赞
//先判断是否点过赞 value是set集合存的是userId
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
}*/
//编程式事务
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
String userLikeKey = RedisUtil.getUserLikeKey(entityUserId);
//判断当前用户有没有点赞,这一步应该在事务开启前执行,因为在事务中的查询不会立即得到结果
Boolean isMember = redisOperations.opsForSet().isMember(entityLikeKey, userId);
//事务开启
redisOperations.multi();
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
//被点赞的用户点赞数量减一
redisOperations.opsForValue().decrement(userLikeKey);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
//被点赞的用户点赞数量加一
redisOperations.opsForValue().increment(userLikeKey);
}
return redisOperations.exec();
}
});
}
3.LikeService增加查询某个用户获得赞的数量
4.重构表现层
detail页面
js
开发个人主页
1.UserController增加找用户页面的方法
@Autowired
private LikeService likeService;
//个人主页
@RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId")int userId,Model model){
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
//用户
model.addAttribute("user",user);
int likeCount = likeService.findUserLikeCount(user.getId());
model.addAttribute("likeCount",likeCount);
return "/site/profile";
}
2.处理index.html中的链接
所有用户头像也得加链接
这里只处理了首页上的,其他的暂时没有处理。
3.处理profile.html
五、关注、取消关注
-
需求
-
- 开发关注、取消关注功能。
- 统计用户的关注数、粉丝数。
-
关键
-
- 若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标)。
- 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体。
RedisUtil中增加获得key的方法
业务层
@Service
public class FollowService {
@Autowired
private RedisTemplate redisTemplate;
//关注
public void follow(int userId,int entityType,int entityId){
//还得依靠事务解决,因为有多次操作
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followee = RedisUtil.getFolloweeKey(userId,entityType);
String follower = RedisUtil.getFollowerKey(entityType,entityId);
//启用事务
redisOperations.multi();
//userId关注entityId
redisOperations.opsForZSet().add(followee,entityId,System.currentTimeMillis());
//entityId的粉丝是userId
redisOperations.opsForZSet().add(follower,userId,System.currentTimeMillis());
return redisOperations.exec();
}
});
}
//取消关注
public void unFollow(int userId,int entityType,int entityId){
//还得依靠事务解决,因为有多次操作
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followee = RedisUtil.getFolloweeKey(userId,entityType);
String follower = RedisUtil.getFollowerKey(entityType,entityId);
//启用事务
redisOperations.multi();
//userId没有关注谁
redisOperations.opsForZSet().remove(followee,entityId);
//谁的粉丝没有userId
redisOperations.opsForZSet().remove(follower,userId);
return redisOperations.exec();
}
});
}
//查询关注的实体的数量
public long findFolloweeCount(int userId,int entityType){
String followee = RedisUtil.getFolloweeKey(userId,entityType);
return redisTemplate.opsForZSet().zCard(followee);
}
//查询实体的粉丝数量
public long findFollowerCount(int entityType,int entityId){
String follower = RedisUtil.getFollowerKey(entityType,entityId);
return redisTemplate.opsForZSet().zCard(follower);
}
//查询当前用户是否关注该实体
public boolean hasFollowed(int userId,int entityType,int entityId){
String followee = RedisUtil.getFolloweeKey(userId,entityType);
return redisTemplate.opsForZSet().score(followee,entityId)!=null?true:false;
}
}
表现层
FollowController
@Controller
public class FollowController {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/follow",method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType,int entityId){
User user = hostHolder.getUser();
if(user==null){
throw new RuntimeException("用户没有登录");
}
followService.follow(user.getId(),entityType,entityId);
return CommunityUtil.getJsonString(0,"已关注");
}
@RequestMapping(path = "/unfollow",method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType,int entityId){
User user = hostHolder.getUser();
if(user==null){
throw new RuntimeException("用户没有登录");
}
followService.unFollow(user.getId(),entityType,entityId);
return CommunityUtil.getJsonString(0,"已取消关注");
}
}
UserController中如下方法增加功能
页面处理
因为各种地方都可以关注,此处只演示在用户主页关注人,其他大同小异
继续改成这样,处理关注按钮
在profile.js中写处理逻辑
$(function(){
$(".follow-btn").click(follow);
});
function follow() {
var btn = this;
if($(btn).hasClass("btn-info")) {
// 关注TA
$.post(
CONTEXT_PATH+"/follow",
{"entityType":3,"entityId":$(btn).prev().val()},
function (data) {
data = $.parseJSON(data);
if(data.code==0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
} else {
// 取消关注
$.post(
CONTEXT_PATH+"/unfollow",
{"entityType":3,"entityId":$(btn).prev().val()},
function (data) {
data = $.parseJSON(data);
if(data.code==0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
//$(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
}
}
继续修改profile.html处理显示
六、关注列表、粉丝列表
-
业务层
-
- 查询某个用户关注的人,支持分页。
- 查询某个用户的粉丝,支持分页。
-
表现层
-
- 处理“查询关注的人”、“查询粉丝”请求。
- 编写“查询关注的人”、“查询粉丝”模板。
业务层
FollowService中增加方法
@Autowired
private UserService userService;
//查询某个用户关注的人
public List<Map<String,Object>> findFollowees(int userId,int offset,int limit){
String followee = RedisUtil.getFolloweeKey(userId,ENTITY_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followee, offset, offset + limit - 1);
if(targetIds==null){
return null;
}
List<Map<String,Object>> list = new ArrayList<>();
for(Integer id:targetIds){
Map<String,Object> map = new HashMap<>();
User user = userService.findUserById(id);
//用户
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followee, id);
//关注时间
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
//查询某个用户的粉丝
public List<Map<String,Object>> findFollowers(int userId,int offset,int limit){
String follower = RedisUtil.getFollowerKey(ENTITY_USER,userId);
//虽然返回的是set但是是redis内置实现了一个set可以有序排列
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(follower, offset, offset + limit - 1);
if(targetIds==null){
return null;
}
List<Map<String,Object>> list = new ArrayList<>();
for(Integer id:targetIds){
Map<String,Object> map = new HashMap<>();
User user = userService.findUserById(id);
//用户
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(follower, id);
//关注时间
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
表现层
1.某个用户关注了谁—FollowController增加如下方法
@Autowired
private UserService userService;
@RequestMapping(path="/followees/{userId}",method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId")int userId, Page page, Model model){
//页面需要用username
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
model.addAttribute("user",user);
page.setLimit(5);
page.setPath("/followees/"+userId);
page.setRows((int)followService.findFolloweeCount(userId,ENTITY_USER));
List<Map<String, Object>> followees = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if(followees!=null){
for(Map<String,Object> map:followees){
//判段当前用户对这个用户的关注状态
User followeeUser = (User)map.get("user");
boolean hasFollowed = hasFollowed(followeeUser.getId());
map.put("hasFollowed",hasFollowed);
}
}
model.addAttribute("users",followees);
return "/site/followee";
}
private boolean hasFollowed(int userId){
if(hostHolder.getUser()==null){
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(),CommunityContant.ENTITY_USER,userId);
}
2.某个用户的粉丝
@RequestMapping(path="/followers/{userId}",method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId")int userId, Page page, Model model){
//页面需要用username
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
model.addAttribute("user",user);
page.setLimit(5);
page.setPath("/followers/"+userId);
page.setRows((int)followService.findFollowerCount(ENTITY_USER,userId));
List<Map<String, Object>> followers = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if(followers!=null){
for(Map<String,Object> map:followers){
//判段当前用户对这个用户的关注状态
User followeeUser = (User)map.get("user");
boolean hasFollowed = hasFollowed(followeeUser.getId());
map.put("hasFollowed",hasFollowed);
}
}
model.addAttribute("users",followers);
return "/site/follower";
}
处理页面
1.profile.html
2.followee.html
3.follower.html
同理,仔细点尤其是处理已关注未关注按钮那里
七、优化登录模块
-
使用Redis存储验证码
-
- 验证码需要频繁的访问与刷新,对性能要求较高。
- 验证码不需永久保存,通常在很短的时间后就会失效。
- 分布式部署时,存在Session共享的问题。
-
使用Redis存储登录凭证
-
- 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。
-
使用Redis缓存用户信息
-
- 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高。
我们可以实现从redis中获取验证码,首先当客户端获取验证码时,先随机生成一个值,这个值作为是客户的登录凭证,我们将这个凭证设置时间为60秒,到时间自动删除。
使用Redis存储验证码
最初,我们把验证码存在了session里,这样不好。使用Redis存验证码的好处:
- Redis性能较高
- Redis可以设置失效时间
- 存到Redis里分布式部署的时候避免了Session共享的问题
1.RedisUtil中增加存储验证码的key
2.LoginController里方法
修改getKaptcha方法
修改Login方法
使用Redis存储登录凭证
最初,我们把登陆凭证存到了MySql里,每次都需要频繁的查询,因为设置了拦截器查询登陆状态。替换用login_ticket表存数据
1.RedisUtil中定义key
2.LoginTicketMapper加@Deprecated注解表名不推荐使用
3.重构代码主要集中在UserService和LoginService中
1.LoginService中login方法
2.LoginService中logout方法
3.UserService中findLoginTicket方法
使用Redis缓存用户信息
就是查询用户的时候先从Redis中取,没有的话先从数据库中取然后存到redis中。用户状态变化时直接删除Redis中的数据。
1.RedisUtil中增加key
2.在UserService中封装三个方法:
- 从redis中取数据
- redis没有的时候数据库找到user后写入redis
- 数据变更时删除缓存
3.其他涉及查询User的地方调这三个方法
当然还有其他可以修改的