4.1 Redis入门
https://redis.io
redis官网只提供了Linux环境下的安装包,没有提供针对windows的安装包,但是微软提供了针对windows环境下的redis安装包。 https://github.com/MicrosoftArchive/redis/releases
windows下的redis安装包各个版本放到了阿里云盘,自取:
redis-for-windows
提取码: 80im
安装完成之后:
在windows下我们访问redis客户端通常会通过命令行的方式访问,为了方便访问,我们把安装路径配到环境变量里,这样命令行就可以直接访问到这些工具了。
redis安装完成之后服务就自动启动了,接下来我们来启用一下redis的客户端。
使用命令 redis-cli
redis提倡如果一个名字由两个单次构成,建议中间加上 冒号
redis操作相关数据的命令可以看我以前写的博客:
Redis指定配置文件启动、数据库相关指令以及Redis操作String类型、List类型
Redis操作Set、Zset、Hash数据类型以及可视化工具的使用
相关指令
# 1.DEL指令
- 语法 : DEL key (如果要删除多个key,不同key之间可以用空格隔开,例如: DEL key1 key2 key3)
- 作用 : 删除给定的一个或多个key 。不存在的key 会被忽略。
- 可用版本: >= 1.0.0
- 返回值: 被删除key 的数量。
# 2.EXISTS指令
- 语法: EXISTS key (如果要判断多个key 不同key之间用空格隔开 例如:EXISTS key1 key2 key3 只要存在1 个,就返回存在的数量)
- 作用: 检查给定key 是否存在。
- 可用版本: >= 1.0.0
- 返回值: 若key 存在,返回1 ,否则返回0。
# 3.EXPIRE
- 语法: EXPIRE key seconds
- 作用: 为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
- 可用版本: >= 1.0.0
- 时间复杂度: O(1)
- 返回值:设置成功返回1 。
# 4.KEYS
- 语法 : KEYS pattern
- 作用 : 查找所有符合给定模式pattern 的key 。
- 语法:
KEYS * 匹配数据库中所有key 。
KEYS h?llo 匹配hello ,hallo 和hxllo 等。 (?代表一个任意字符,不能代表0个字符)
KEYS h*llo 匹配hllo 和heeeeello 等。 (*代表任意多个字符,可以是0个)
KEYS h[ae]llo 匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开([]也是只能匹配一个,只能匹 配hallo或者是hello,如果想要匹配多个,可以使用h[ae][ae]llo )
- 可用版本: >= 1.0.0
- 返回值: 符合给定模式的key 列表。
# 5.MOVE
- 语法 : MOVE key db
- 作用 : 将当前数据库的key 移动到给定的数据库db 当中。
- 可用版本: >= 1.0.0
- 返回值: 移动成功返回1 ,失败则返回0 。
# 6.PEXPIRE
- 语法 : PEXPIRE key milliseconds
- 作用 : 这个命令和EXPIRE 命令的作用类似,但是它以毫秒为单位设置key 的生存时间,而不像EXPIRE 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 时间复杂度: O(1)
- 返回值:设置成功,返回1 key 不存在或设置失败,返回0
# 7.PEXPIREAT
- 语法 : PEXPIREAT key milliseconds-timestamp
- 作用 : 这个命令和EXPIREAT 命令类似,但它以毫秒为单位设置key 的过期unix 时间戳,而不是像EXPIREAT那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值:如果生存时间设置成功,返回1 。当key 不存在或没办法设置生存时间时,返回0 。(查看EXPIRE 命令获取更多信息)
# 8.TTL
- 语法 : TTL key
- 作用 : 以秒为单位,返回给定key 的剩余生存时间(TTL, time to live)。
- 可用版本: >= 1.0.0
- 返回值:
当key 不存在时,返回-2 。
当key 存在但没有设置剩余生存时间时,返回-1 。
否则,以秒为单位,返回key 的剩余生存时间。
- Note : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
# 9.PTTL
- 语法 : PTTL key
- 作用 : 这个命令类似于TTL 命令,但它以毫秒为单位返回key 的剩余生存时间,而不是像TTL 命令那样,以秒为单位。
- 可用版本: >= 2.6.0
- 返回值: 当key 不存在时,返回-2 。当key 存在但没有设置剩余生存时间时(即永久存储),返回-1 。
- 否则,以毫秒为单位,返回key 的剩余生存时间。
- 注意 : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
# 10.RANDOMKEY
- 语法 : RANDOMKEY
- 作用 : 从当前数据库中随机返回(不删除) 一个key 。
- 可用版本: >= 1.0.0
- 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil 。
# 11.RENAME
- 语法 : RENAME key newkey
- 作用 : 将key 改名为newkey 。当key 和newkey 相同,或者key 不存在时,返回一个错误。当newkey 已经存在时,RENAME 命令将覆盖旧值。
- 可用版本: >= 1.0.0
- 返回值: 改名成功时提示OK ,失败时候返回一个错误。
# 12.TYPE
- 语法 : TYPE key
- 作用 : 返回key 所储存的值(value)的类型。
- 可用版本: >= 1.0.0
- 返回值:
none (key 不存在)
string (字符串)
list (列表)
set (集合)
zset (有序集)
hash (哈希表)
Redis操作String类型数据
1. 存存储模型
2. 常用操作命令
命令 | 说明 |
set | 设置一个key/value |
get | 根据key获得对应的value |
mset | 一次设置多个key value |
mget | 一次获得多个key的value |
getset | 获得原始key对应的value,同时设置新的value |
strlen | 获得指定key对应的value的长度 |
append | 为key对应的value追加内容并返回追加之后value的长度 |
getrange | 获取指定key对应的value指定索引范围的内容 |
setex | 在设置一个key的时候就指定存活的有效期(单位: 秒) |
psetex | 在设置一个key的时候就指定存活的有效期(单位: 毫秒) |
setnx | 存在key时不做任何操作,不存在添加 |
msetnx 原子操作(只要有一个存在不做任何操作) | 可以同时设置多个key,只有有一个存在都不保存 |
decr | 若key对应的value为数值类型(数值串)对value进行-1操作 |
decrby | 若key对应的value为数值类型(数值串)对value减去指定数值 |
Incr | 对key对应的value数值类型(数值串)进行+1操作 |
incrby | 对key对应的value数值类型(数值串)加上指定数值 |
Incrbyfloat | 对key对应的value数值类型(数值串)加上浮点数 |
Redis操作List类型数据
1.内存存储模型
2.常用操作指令
命令 | 说明 |
lpush | 存在列表就直接放,不存在就先创建列表再放,将某个值(某些值)加入到key列表头部 |
lpushx | 同lpush,但是必须要保证这个key存在 |
rpush | 存在列表就直接放,不存在就先创建列表再放,将某个值(某些值)加入到key列表尾部 |
rpushx | 同rpush,但是必须要保证这个key存在 |
lpop | 返回和移除列表首部的第一个元素 |
rpop | 返回和移除列表尾部的第一个元素 |
lrange | 获取key对应的列表指定下标区间内的元素 |
llen | 获取key对应的列表元素个数 |
lset | 设置key对应的列表某一个指定索引的值(索引必须存在) |
lindex | 获取key对应的列表某一个指定索引位置的元素 |
lrem | 从key对应的列表中从左到右删除指定数量的对应元素 |
ltrim | 只保留列表中特定区间内的元素,删除其他元素 |
linsert | 在某一个元素之前,之后插入新元素 |
Redis操作Set类型数据
1.内存存储模型
2.常用操作指令
命令 | 说明 |
sadd | 没有Set时创建,之后向key对应的类型为Set的value添加元素 |
smembers | 显示key对应的Set集合中所有元素 (无序) |
scard | 返回key对应的Set集合中元素的个数 |
spop | 随机返回一个(多个)元素 并将元素在Set集合中删除 |
smove | 从一个Set集合中向另一个Set集合移动元素 必须是同一种类型 |
srem | 从key对应的Set集合中删除指定的一个(多个)元素 |
sismember | 判断key对应的Set集合中是否含有这个元素 |
srandmember | 随机返回key对应的Set集合中的一个(多个)元素 |
sdiff | 展示去掉第一个集合中其它集合含有的相同元素(仅仅只是展示,在数据库中并没有实际删除) |
sinter | 求交集 |
sunion | 求和集 |
Redis操作Zset类型数据
1.内存模型
2.常用命令
命令 | 说明 |
zadd | 没有Zset时先创建,添加一个有序集合元素 |
zcard | 返回集合的元素个数 |
zrange 升序 zrevrange 降序 | 返回升序|降序的元素列表,Zset根据得分进行排序 |
zrangebyscore | 按照分数查找一个范围内的元素 |
zrank | 返回排名 |
zrevrank | 倒序排名 |
zscore | 显示某一个元素的分数 |
zrem | 移除某一个元素 |
zincrby | 给某个特定元素加分 |
Redis操作Hash类型数据
1. 内存模型
2.常用命令
命令 | 说明 |
hset | 设置一个key为String,value为key-value的键值对 |
hget | 获得一个key对应的value |
hgetall | 获得所有的key/value对 |
hdel | 删除某一个key/value对 |
hexists | 判断一个key是否存在 |
hkeys | 获得所有的key |
hvals | 获得所有的value |
hmset | 设置多个key/value |
hmget | 获得多个key的value |
hsetnx | 设置一个不存在的key的值 |
hincrby | 为value进行加法运算 |
hincrbyfloat | 为value加入浮点值 |
4.2 Spring整合Redis
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置Redis
在 application.properties 配置文件中配置 redis 相关的
# RedisProperties 配置redis相关的(使用redis中的哪个数据库、redis所在服务器的ip、redis服务器的端口)
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379
编写配置类,构造RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 方法参数 RedisConnectionFactory 会自动被工厂注入
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;
}
}
3.访问Redis
@SpringBootTest
@RunWith(SpringRunner.class)
@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));
}
/*
结果:
1
2
1
*/
@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"));
}
/*
结果:
1
zhangsan
*/
@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));
}
/*
结果:
3
103
[103, 102, 101]
103
102
101
*/
@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));
}
/*
结果:
5
诸葛亮
[张飞, 关羽, 赵云, 刘备]
*/
@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, "八戒"));
// reverseRank:由大到小 rank:由小到大
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
/*
结果:
5
50.0
4
[悟空, 唐僧, 沙僧]
*/
@Test
public void testKeys() {
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}
/*
结果:
false
*/
/**
* 有可能每次访问的时候都访问同一个key,每个api都要把这个key传进去,
* 有点麻烦,针对这样的逻辑我们可以绑定这个key,每次操作都一定是针对
* 这个key的,不用传key了。
*/
// 多次访问同一个key的话我们可以绑定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());
}
/*
结果:
6
*/
// 编程式事务
@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");
System.out.println(operations.opsForSet().members(redisKey));
return operations.exec(); // 提交事务
}
});
System.out.println(obj);
}
/*
结果:
[]
[1, 1, 1, [zhangsan, wangwu, lisi]]
*/
}
重点:redis中事务
因为redis是一个数据库,它也是支持事务的,但是它所支持的事务的机制不完全满足ACID四个特性,因为毕竟它不是关系型数据库,只有关系型数据库才严格满足这四个特点,整体来说redis的事务管理是比较简单的。它的机制是:当我启用事务以后,当我再去执行一个redis命令的时候,它并不会立刻执行这个命令,而是把这个命令放到一个队列里先存着,然后再执行一个命令再放到队列里,直到提交事务的时候,它会把队列中的命令一股脑发给redis服务器一起执行。这里有一个隐含的问题需要注意:因为事务之内的命令不会立刻执行,而是提交时统一批量的执行,所以如果在事务的过程中做了一个查询(这个查询不受事务管理),这个查询不会立刻返回结果,也就是说不要在事务中间去做查询,要么提交查,要么事务提交之后查!!!
Spring支持redis的声明式事务和编程式事务,也是声明式事务更简单,只要做一些配置,加上@Transactional注解就可以了,但是因为redis的事务有刚才所说的问题的存在,所以我们通常不会用声明式事务,因为声明式事务只能精确到一个方法,在方法上加上@Transactional,方法内部整个逻辑都是整个的事务范围,这个方法之内就没办法去查询了。所以我们通常都用编程式事务把事务的范围缩小。
编程式事务格式:
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
// 编程式事务
@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");
System.out.println(operations.opsForSet().members(redisKey));
return operations.exec(); // 提交事务
}
});
System.out.println(obj);
}
/*
结果:
[]
[1, 1, 1, [zhangsan, wangwu, lisi]]
*/
}
4.3 点赞
因为点赞频率非常高,如果把数据存到硬盘里,读写性能很差,所以我们把数据存到redis里,存到内存里性能非常好,还可以使用redis的持久化机制将数据从内存存到硬盘里。
因为之前开发的时候是存到硬盘里,开发顺序 dao -> service -> controller,但是因为我们这次是存到redis里,直接写redis命令就可以,所以我们数据访问层就不开发了,直接将redis命令写到service层里。
首先我们写一个工具类专门生成redis的key,好复用这个工具类RedisKeyUtil。
配置 RedisTemplate 和 书写工具类生成redis的key
配置 RedisTemplate 之前已写过,所以可以不用写了。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 方法参数 RedisConnectionFactory 会自动被工厂注入
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;
}
}
写一个工具类去生成redis的key
public class RedisKeyUtil {
private static final String SPLIT = ":"; // key中间的 :
private static final String PREFIX_ENTITY_LIKE = "like:entity"; // key的前缀
// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)
// 我们最终要把这个点赞的userId存到set里,因为我们未来可能会开发查看谁给我点赞的功能
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
}
开发 Service 层
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞
public void like(int userId, int entityType, int entityId, int entityUserId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
operations.multi();
if (isMember) {
// 点过赞再点赞就是取消赞
operations.opsForSet().remove(entityLikeKey, userId);
} else {
// 没过点赞的话就是点赞
operations.opsForSet().add(entityLikeKey, userId);
}
return operations.exec();
}
});
}
// 查询某实体(帖子被点赞)点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
// 查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}
}
开发 表现层
这个“点赞”是一个异步请求,所以方法上也要加上 @ResponseBody 注解
@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);
}
}
点赞是在帖子详情页面点的,所以我们需要打开帖子详情页面discuss-detail.html做一个处理:
因为是异步请求,所以我们还需要提供一个js方法来实现点赞提交请求的逻辑。
discuss-detail.html:
我们还需要对首页index.html帖子显示的赞的数量做一个处理
首先是返回首页请求的HomeController
然后是 index.html
然后需要对帖子详情页面显示的赞的数量做一个处理(加一些内容)
DiscussPostController:
然后是详情页面discuss-detail.html
4.4 我收到的赞
重构点赞功能
这里我们处理用户获得的点赞数量,我们可以在点赞的时候把这个赞的数量存到redis里(以用户id为key),所以我们需要重构一下以前写的点赞的代码。
在RedisKeyUtil工具类里写一个新方法设置user存到redis里的key设置成什么
首先是LikeService
然后是LikeController
然后是 discuss-detail.html
开发展示个人主页功能
直接把功能写在 UserController 内
然后是“个人主页”页面profile.html
然后在首页index.html上加上用户头像的访问路径
帖子详情页discuss-detail.html改头像的链接使其跳转到个人主页
私信页面letter.html改头像的链接使其跳转到展示个人主页的controller
私信详情页面letter-detail.html改头像的链接使其跳转到展示个人主页的controller
最后启动项目之后,经自己测试开发成功。