本文主要讲 Redis 的使用,如何与 SpringBoot 项目整合,如何使用注解方式和 RedisTemplate 方式实现缓存。最后会给一个用 Redis 实现分布式锁,用在秒杀系统中的案例。
更多 Redis 的实际运用场景请关注开源项目 coderiver
项目地址:https://github.com/cachecats/coderiver
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。
随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。 -- 百度百科
分类 | 相关产品 | 典型应用 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值(key-value) | Tokyo、 Cabinet/Tyrant、Redis、Voldemort、Berkeley DB | 内容缓存,主要用于处理大量数据的高访问负载 | 一系列键值对 | 快速查询 | 存储的数据缺少结构化 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档数据库 | CouchDB, MongoDB | Web应用(与Key-Value类似,value是结构化的) | 一系列键值对 | 数据结构要求不严格 | 查询性能不高,而且缺乏统一的查询语法 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法 | 需要对整个图做计算才能得出结果,不容易做分布式集群方案 |
网上有很多 Redis 的安装教程,这里就不多说了,只说下 Docker 的安装方法:
Docker 安装运行 Redis
docker run -d -p 6379:6379 redis:4.0.8
如果以后想启动 Redis 服务,打开命令行,输入以下命令即可。
redis-server
使用前先引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
使用缓存有两个前置步骤
常用的注解有以下几个
@Cacheable
属性如下图
用于查询和添加缓存,第一次查询的时候返回该方法返回值,并向 Redis 服务器保存数据。
以后调用该方法先从 Redis 中查是否有数据,如果有直接返回 Redis 缓存的数据,而不执行方法里的代码。如果没有则正常执行方法体中的代码。
value 或 cacheNames 属性做键,key 属性则可以看作为 value 的子键, 一个 value 可以有多个 key 组成不同值存在 Redis 服务器。
验证了下,value 和 cacheNames 的作用是一样的,都是标识主键。两个属性不能同时定义,只能定义一个,否则会报错。
condition 和 unless 是条件,后面会讲用法。其他的几个属性不常用,其实我也不知道怎么用…
@CachePut
更新 Redis 中对应键的值。属性和 @Cacheable
相同
@CacheEvict
删除 Redis 中对应键的值。
在需要加缓存的方法上添加注解 @Cacheable(cacheNames = "product", key = "123")
,
cacheNames
和 key
都必须填,如果不填 key
,默认的 key
是当前的方法名,更新缓存时会因为方法名不同而更新失败。
如在订单列表上加缓存
@RequestMapping(value = "/list", method = RequestMethod.GET)
@Cacheable(cacheNames = "product", key = "123")
public ResultVO list() {
<span class="hljs-comment">// 1.查询所有上架商品</span>
List<ProductInfo> productInfoList = productInfoService.findUpAll();
<span class="hljs-comment">// 2.查询类目(一次性查询)</span>
<span class="hljs-comment">//用 java8 的特性获取到上架商品的所有类型</span>
List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);
List<ProductVO> productVOList = <span class="hljs-keyword">new</span> ArrayList<>();
<span class="hljs-comment">//数据拼装</span>
<span class="hljs-keyword">for</span> (ProductCategory category : productCategoryList) {
ProductVO productVO = <span class="hljs-keyword">new</span> ProductVO();
<span class="hljs-comment">//属性拷贝</span>
BeanUtils.copyProperties(category, productVO);
<span class="hljs-comment">//把类型匹配的商品添加进去</span>
List<ProductInfoVO> productInfoVOList = <span class="hljs-keyword">new</span> ArrayList<>();
<span class="hljs-keyword">for</span> (ProductInfo productInfo : productInfoList) {
<span class="hljs-keyword">if</span> (productInfo.getCategoryType().equals(category.getCategoryType())) {
ProductInfoVO productInfoVO = <span class="hljs-keyword">new</span> ProductInfoVO();
BeanUtils.copyProperties(productInfo, productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}
<span class="hljs-keyword">return</span> ResultVOUtils.success(productVOList);
}
可能会报如下错误
对象未序列化。让对象实现 Serializable
方法即可
@Data
public class ProductVO implements Serializable {
private static final long serialVersionUID = 961235512220891746L;
@JsonProperty(<span class="hljs-string">"name"</span>)
private String categoryName;
@JsonProperty(<span class="hljs-string">"type"</span>)
private Integer categoryType;
@JsonProperty(<span class="hljs-string">"foods"</span>)
private List<ProductInfoVO> productInfoVOList ;
}
生成唯一的 id 在 IDEA 里有一个插件:GenerateSerialVersionUID
比较方便。
重启项目访问订单列表,在 rdm 里查看 Redis 缓存,有 product::123
说明缓存成功。
在需要更新缓存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")
注意
cacheNames
和 key
要跟 @Cacheable()
里的一致,才会正确更新。
@CachePut()
和 @Cacheable()
注解的方法返回值要一致
在需要删除缓存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123")
,执行完这个方法之后会将 Redis 中对应的记录删除。
如果参数是个对象,也可以设置对象的某个属性为 key。比如其中一个参数是 user 对象,key 可以写成 key="#user.id"
还可以指定 unless
即条件不成立时缓存。#result
代表返回值,意思是当返回码不等于 0 时不缓存,也就是等于 0 时才缓存。
@GetMapping("/detail")
@Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0")
public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
@RequestParam("orderId") String orderId){
OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);
return ResultVOUtils.success(orderDTO);
}
与使用注解方式不同,注解方式可以零配置,只需引入依赖并在启动类上加上 @EnableCaching
注解就可以使用;而使用 RedisTemplate 方式麻烦些,需要做一些配置。
第一步还是引入依赖和在启动类上加上 @EnableCaching
注解。
然后在 application.yml
文件中配置 Redis
spring:
redis:
port: 6379
database: 0
host: 127.0.0.1
password:
jedis:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
timeout: 5000ms
然后写个 RedisConfig.java
配置类
package com.solo.coderiver.user.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.net.UnknownHostException;
@Configuration
public class RedisConfig {
<span class="hljs-meta">@Bean</span>
<span class="hljs-meta">@ConditionalOnMissingBean</span>(name = <span class="hljs-string">"redisTemplate"</span>)
<span class="hljs-function"><span class="hljs-keyword">public</span> RedisTemplate<String, Object> <span class="hljs-title">redisTemplate</span><span class="hljs-params">(
RedisConnectionFactory redisConnectionFactory)</span>
<span class="hljs-keyword">throws</span> UnknownHostException </span>{
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = <span class="hljs-keyword">new</span> Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = <span class="hljs-keyword">new</span> ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> template = <span class="hljs-keyword">new</span> RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
<span class="hljs-keyword">return</span> template;
}
<span class="hljs-meta">@Bean</span>
<span class="hljs-meta">@ConditionalOnMissingBean</span>(StringRedisTemplate.class)
<span class="hljs-function"><span class="hljs-keyword">public</span> StringRedisTemplate <span class="hljs-title">stringRedisTemplate</span><span class="hljs-params">(
RedisConnectionFactory redisConnectionFactory)</span>
<span class="hljs-keyword">throws</span> UnknownHostException </span>{
StringRedisTemplate template = <span class="hljs-keyword">new</span> StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
<span class="hljs-keyword">return</span> template;
}
}
Redis 的配置就完成了。
Redis 可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。
下面来对这5种数据结构类型作简单的介绍:
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
String | 可以是字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement) |
List | 一个链表,链表上的每个节点都包含了一个字符串 | 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素 |
Set | 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 | 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素 |
Hash | 包含键值对的无序散列表 | 添加、获取、移除单个键值对;获取所有键值对 |
Zset | 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 | 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素 |
RedisTemplate 对五种数据结构分别定义了操作
如果操作字符串的话,建议用 StringRedisTemplate
。
在需要使用 Redis 的地方,用 @Autowired
注入进来
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
由于项目中暂时仅用到了 StringRedisTemplate 与 RedisTemplate 的 Hash 结构,StringRedisTemplate 比较简单就不贴代码了,下面仅对操作 Hash 进行举例。
关于 RedisTemplate 的详细用法,有一篇文章已经讲的很细很好了,我觉得没必要再去写了。传送门
package com.solo.coderiver.user.service.impl;
import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.utils.RedisKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class RedisServiceImpl implements RedisService {
<span class="hljs-meta">@Autowired</span>
RedisTemplate redisTemplate;
<span class="hljs-meta">@Autowired</span>
LikedService likedService;
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">saveLiked2Redis</span><span class="hljs-params">(String likedUserId, String likedPostId)</span> </span>{
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">unlikeFromRedis</span><span class="hljs-params">(String likedUserId, String likedPostId)</span> </span>{
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">deleteLikedFromRedis</span><span class="hljs-params">(String likedUserId, String likedPostId)</span> </span>{
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">incrementLikedCount</span><span class="hljs-params">(String likedUserId)</span> </span>{
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, <span class="hljs-number">1</span>);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">decrementLikedCount</span><span class="hljs-params">(String likedUserId)</span> </span>{
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -<span class="hljs-number">1</span>);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> List<UserLike> <span class="hljs-title">getLikedDataFromRedis</span><span class="hljs-params">()</span> </span>{
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
List<UserLike> list = <span class="hljs-keyword">new</span> ArrayList<>();
<span class="hljs-keyword">while</span> (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String key = (String) entry.getKey();
<span class="hljs-comment">//分离出 likedUserId,likedPostId</span>
String[] split = key.split(<span class="hljs-string">"::"</span>);
String likedUserId = split[<span class="hljs-number">0</span>];
String likedPostId = split[<span class="hljs-number">1</span>];
Integer value = (Integer) entry.getValue();
<span class="hljs-comment">//组装成 UserLike 对象</span>
UserLike userLike = <span class="hljs-keyword">new</span> UserLike(likedUserId, likedPostId, value);
list.add(userLike);
<span class="hljs-comment">//存到 list 后从 Redis 中删除</span>
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}
<span class="hljs-keyword">return</span> list;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> List<LikedCountDTO> <span class="hljs-title">getLikedCountFromRedis</span><span class="hljs-params">()</span> </span>{
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
List<LikedCountDTO> list = <span class="hljs-keyword">new</span> ArrayList<>();
<span class="hljs-keyword">while</span> (cursor.hasNext()) {
Map.Entry<Object, Object> map = cursor.next();
<span class="hljs-comment">//将点赞数量存储在 LikedCountDT</span>
String key = (String) map.getKey();
LikedCountDTO dto = <span class="hljs-keyword">new</span> LikedCountDTO(key, (Integer) map.getValue());
list.add(dto);
<span class="hljs-comment">//从Redis中删除这条记录</span>
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
}
<span class="hljs-keyword">return</span> list;
}
}
讲完了基础操作,再说个实战运用,用Redis 实现分布式锁 。
实现分布式锁之前先看两个 Redis 命令:
返回值
bulk-string-reply: 返回之前的旧值,如果之前Key
不存在将返回nil
。
例子
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis>
这两个命令在 java 中对应为 setIfAbsent
和 getAndSet
分布式锁的实现:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Slf4j
public class RedisLock {
<span class="hljs-meta">@Autowired</span>
StringRedisTemplate redisTemplate;
<span class="hljs-comment">/**
* 加锁
* <span class="hljs-doctag">@param</span> key
* <span class="hljs-doctag">@param</span> value 当前时间 + 超时时间
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">lock</span><span class="hljs-params">(String key, String value)</span></span>{
<span class="hljs-keyword">if</span> (redisTemplate.opsForValue().setIfAbsent(key, value)){
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}
<span class="hljs-comment">//解决死锁,且当多个线程同时来时,只会让一个线程拿到锁</span>
String currentValue = redisTemplate.opsForValue().get(key);
<span class="hljs-comment">//如果过期</span>
<span class="hljs-keyword">if</span> (!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
<span class="hljs-comment">//获取上一个锁的时间</span>
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
<span class="hljs-keyword">if</span> (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
<span class="hljs-comment">/**
* 解锁
* <span class="hljs-doctag">@param</span> key
* <span class="hljs-doctag">@param</span> value
*/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">unlock</span><span class="hljs-params">(String key, String value)</span></span>{
<span class="hljs-keyword">try</span> {
String currentValue = redisTemplate.opsForValue().get(key);
<span class="hljs-keyword">if</span> (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}<span class="hljs-keyword">catch</span> (Exception e){
log.error(<span class="hljs-string">"【redis锁】解锁失败, {}"</span>, e);
}
}
}
使用:
/**
* 模拟秒杀
*/
public class SecKillService {
@Autowired
RedisLock redisLock;
//超时时间10s
private static final int TIMEOUT = 10 * 1000;
public void secKill(String productId){
long time = System.currentTimeMillis() + TIMEOUT;
//加锁
if (!redisLock.lock(productId, String.valueOf(time))){
throw new SellException(101, "人太多了,等会儿再试吧~");
}
//具体的秒杀逻辑
//解锁
redisLock.unlock(productId, String.valueOf(time));
}
}
更多 Redis 的具体使用场景请关注开源项目 CodeRiver
,致力于打造全平台型全栈精品开源项目。