文章目录
- 一、背景
一、背景
- 本篇文章是将以前Redis实战的系列文章进行汇总,针对Redis中常用的一些数据结构,进行实战模拟。
strings | hashes | lists | sets | sorted sets |
封锁一个IP地址 | 存储用户信息 | 模拟消息队列 | 自动排重 | 以某一个条件为权重,进行排序 |
1.1 开发环境
1. JDK 1.8
2. SpringBoot 2.2.5
3. JPA
4. Spring Security
5. Mysql 8.0
6. Redis Server 3.2.1
7. Redis Desktop Manager
8. Swagger2
1.2 项目配置
- SpringBoot集成Redis, 添加依赖
<!--pom.xl-->
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 项目配置文件 添加redis连接配置
<!-- application.yml-->
server:
port: 8000
spring:
freemarker:
check-template-location: false
profiles:
active: dev
jackson:
time-zone: GMT+8
data:
redis:
repositories:
enabled: false
#配置 Jpa
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: true
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
- 增加RedisConfig配置类
/**
* Redis配置类
*
* @author zhuhuix
*/
@Configuration
@EnableCaching
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig extends CachingConfigurerSupport {
/**
*设置 redis 数据默认过期时间
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(){
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(Constant.CACHE_TIMEOUT_HOUR));
return configuration;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
/**
* 参考:https://blog.csdn.net/qq_15071263/article/details/84335632
* 自定义缓存key生成策略,默认将使用该策略
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String,Object> container = new HashMap<>(3);
Class<?> targetClassClass = target.getClass();
// 类地址
container.put("class",targetClassClass.toGenericString());
// 方法名称
container.put("methodName",method.getName());
// 包名称
container.put("package",targetClassClass.getPackage());
// 参数列表
for (int i = 0; i < params.length; i++) {
container.put(String.valueOf(i),params[i]);
}
// 转为JSON字符串
String jsonString = JSON.toJSONString(container);
// 做SHA256 Hash计算,得到一个SHA256摘要作为Key
return DigestUtils.sha256Hex(jsonString);
};
}
- 增加RedisUtils工具类:实现对各种数据结构的封装
/**
* Redis工具类
*
* @author zhuhuix
*/
@Component
@AllArgsConstructor
public class RedisUtils {
private RedisTemplate<Object, Object> redisTemplate;
/**
* HashGet根据键值得到对象
*
* @param key 键值 @NotNull
* @param item 项目 @NotNull
* @return 对象
*/
public Object hashGet(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 根据键值向hash表中写入对象
*
* @param key 键值 @NotNull
* @param item 项目 @NotNull
* @param value 对象 @NotNull
* @return true 成功 false失败
*/
public boolean hashSet(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据键值向hash表中写入对象,并设置过期时间
*
* @param key 键值 @NotNull
* @param item 项目 @NotNull
* @param value 对象 @NotNull
* @param time 过期时间(秒) @NotNull
* @return true 成功 false失败
*/
public boolean hashSet(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据键值对某一项目的进行累加计数
*
* @param key 键值
* @param l 累加数
*/
public long increment(String key, long l) {
return redisTemplate.opsForValue().increment(key, l);
}
/**
* 根据键值对某一项目的进行累加计数,并设置过期时间
*
* @param key 键值
* @param l 累加数
* @param time 过期时间(秒)
*/
public long increment(String key, long l, long time) {
long count = redisTemplate.opsForValue().increment(key, l);
if (time > 0) {
expire(key, time);
}
return count;
}
/**
* 入队
*
* @param key 队列键值
* @param value 元素
* @return 添加数量
*/
public long leftPush(String key, Object value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 向队列头部添加全部集合元素
*
* @param key 队列键值
* @param list 集合
* @return 返回添加的数量
*/
public long leftPushAll(String key, List<Object> list) {
return redisTemplate.opsForList().leftPushAll(key, list);
}
/**
* 统计队列中所有元素数量
*
* @param key 队列键值
* @return 队列中元素数量
*/
public long size(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 返回队列中从起始位置到结束位置的集合元素
*
* @param key 队列键值
* @param start 起始位置
* @param end 结束位置
* @return 返回集合
*/
public List<Object> range(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 出队
*
* @param key 队列键值
* @return 元素
*/
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* 弹出队列最新元素
*
* @param key 队列键值
* @return 元素
*/
public Object leftPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
/**
* 删除队列所有元素
*
* @param key 队列键值
*/
public void deleteAll(String key) {
redisTemplate.opsForList().trim(key, 0, 0);
redisTemplate.opsForList().leftPop(key);
}
/**
* 向集合中增加元素
*
* @param key 集合键值
* @param value 元素
* @return 添加数量
*/
public long setAdd(String key, Object value) {
return redisTemplate.opsForSet().add(key,value);
}
/**
* 向集合中批量增加元素
*
* @param key 集合键值
* @param list 元素列表
* @return 添加数量
*/
public long setAdd(String key, List<Object> list) {
return redisTemplate.opsForSet().add(key,list);
}
/**
* 集合删除指定元素
*
* @param key 集合键值
* @param value 指定元素
* @return 删除数量
*/
public long setRemove(String key, Object value) {
return redisTemplate.opsForSet().remove(key, value);
}
/**
* 集合批量删除指定元素
*
* @param key 集合键值
* @param list 指定元素列表
* @return 删除数量
*/
public long setRemove(String key, List<Object> list) {
return redisTemplate.opsForSet().remove(key, list);
}
/**
* 取出两信集合的交集
*
* @param key1 集合1键值
* @param key2 集合2键值
* @return 交集
*/
public Set<Object> setInter(String key1, String key2) {
return redisTemplate.opsForSet().intersect(key1, key2);
}
/**
* 取出多个集合的交集
*
* @param keys 键值列表
* @return 交集
*/
public Set<Object> setInter(List<Object> keys) {
return redisTemplate.opsForSet().intersect(keys);
}
/**
* 取出两个集合的差集
*
* @param key1 集合1键值
* @param key2 集合2键值
* @return 差集
*/
public Set<Object> setDifference(String key1,String key2){
return redisTemplate.opsForSet().difference(key1,key2);
}
/**
* 指定缓存的失效时间
*
* @param key 键值 @NotNull
* @param time 时间(秒) @NotNull
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
二、字符串的应用场景:封锁一个IP地址
- 创建SpringBoot后台服务程序,实现用户登录及JWT认证;
- 通过Redis缓存限制在1分钟内同一IP请求登录不能超过5次。
- 登录实现类增加Redis计数判断
/**
* 授权登录接口实现类
*
* @author zhuhuix
*/
@Slf4j
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class AuthServiceImpl implements AuthService {
@Value("${wxMini.appId}")
private String appId;
@Value("${wxMini.secret}")
private String secret;
private final JwtTokenUtils jwtTokenUtils;
private final WxMiniApi wxMiniApi;
private final UserService userService;
private final JwtSecurityProperties properties;
private final RedisUtils redisUtils;
public AuthServiceImpl(JwtTokenUtils jwtTokenUtils, WxMiniApi wxMiniApi, UserService userService, JwtSecurityProperties properties, RedisUtils redisUtils) {
this.jwtTokenUtils = jwtTokenUtils;
this.wxMiniApi = wxMiniApi;
this.userService = userService;
this.properties = properties;
this.redisUtils = redisUtils;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<AuthUserDto> login(AuthUserDto authUserDto, HttpServletRequest request) {
// 通过缓存判断同一IP某一时间段内的登录次数是否超出限定次数
String ip = NetworkUtils.getIp(request);
String requestLoginIp = "request_login_".concat(ip);
long loginCount = redisUtils.increment(requestLoginIp, 1L);
if (loginCount == 1) {
redisUtils.expire(requestLoginIp, Constant.REQUEST_LOGIN_LIMIT_TIME);
}
if (loginCount > Constant.REQUEST_LOGIN_LIMIT_COUNT) {
log.warn("IP:[".concat(ip).concat("]已超出限定次数"));
throw new RuntimeException("时间段内已超出限定次数,请不要频繁登录!");
}
...
...
}
}
- 测试与验证
三、Hash的应用场景:存储用户信息
– 创建SpringBoot后台服务程序,实现微信小程序登录及JWT认证;
– 通过Redis缓存记录该用户最后一次登录时间及登录累计次数。
/**
* 授权登录接口实现类--增加redis缓存哈希表应用
*
* @author zhuhuix
*/
@Slf4j
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class AuthServiceImpl implements AuthService {
....
....
// 将当前用户信息与登录时间写入Redis缓存的哈希表
// 以微信登录用户的openId作为哈希键值
String key = authUserDto.getUserInfo().getOpenId();
redisUtils.hashSet(key, "id", authUserDto.getUserInfo().getId());
redisUtils.hashSet(key, "nickName", authUserDto.getUserInfo().getNickName());
redisUtils.hashSet(key, "getAvatarUrl", authUserDto.getUserInfo().getAvatarUrl());
redisUtils.hashSet(key, "lastLoginTime", Timestamp.valueOf(LocalDateTime.now()));
// 读取缓存中当前哈希值对应的用户的登录次数
Long loginCount = 1L;
Object obj = redisUtils.hashGet(key, "loginCount");
if (obj != null) {
loginCount += Long.valueOf(String.valueOf(obj));
}
// 累加后回写到哈希表中
redisUtils.hashSet(key, "loginCount", loginCount);
...
}
- 测试与验证
四、List的应用场景:队列实现
- 创建SpringBoot上传文件WebApi服务接口;
- 通过Redis缓存队列记录最新10笔用户上传文件的信息。
- 图片上传服务源码中增加Redis队列
/**
* 微信小程序CRM实现类:实现Redis队列
*
* @author zhuhuix
*/
@Slf4j
@AllArgsConstructor
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class WxMiniCrmImpl implements WxMiniCrm {
...
@Override
@Transactional(rollbackFor = Exception.class)
public Result<CrmIndex> uploadCrmIndex(String json, String openId, String realName, MultipartFile multipartFile) {
try {
JSONObject jsonObject = JSONObject.parseObject(json);
String createTime = jsonObject.getString("create");
String employeeCode = jsonObject.getString("employeeCode");
String customerCode = jsonObject.getString("customerCode");
String customerName = jsonObject.getString("customerName");
String type = jsonObject.getString("type");
if (StringUtils.isEmpty(createTime) || StringUtils.isEmpty(employeeCode) || StringUtils.isEmpty(customerCode)
|| StringUtils.isEmpty(customerName) || StringUtils.isEmpty(type)) {
throw new RuntimeException("上传信息中缺少关键资料");
}
UploadFile uploadFile = uploadFileTool.upload(openId, realName, multipartFile);
if (uploadFile == null) {
throw new RuntimeException("上传文件失败!");
}
CrmIndex crmIndex = new CrmIndex();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
crmIndex.setCreateTime(Timestamp.valueOf(LocalDateTime.parse(createTime, dateTimeFormatter)));
crmIndex.setEmployeeCode(employeeCode);
crmIndex.setCustomerCode(customerCode);
crmIndex.setCustomerName(customerName);
crmIndex.setType(type);
crmIndex.setJson(json);
crmIndex.setOpenId(openId);
crmIndex.setPath(uploadFile.getPath());
// 将最新10条上传的信息放入redis缓存
if (redisUtils.size(Constant.REDIS_UPLOAD_QUEUE_NAME) >= Constant.REDIS_UPLOAD_QUEUE_COUNT) {
log.warn(Constant.REDIS_UPLOAD_QUEUE_NAME.concat("队列已满,移除最旧上传信息:") + redisUtils.rightPop(Constant.REDIS_UPLOAD_QUEUE_NAME));
}
log.info(Constant.REDIS_UPLOAD_QUEUE_NAME.concat("队列增加上传信息:").concat(crmIndex.toString()));
redisUtils.leftPush(Constant.REDIS_UPLOAD_QUEUE_NAME, crmIndex);
return new Result<CrmIndex>().ok(crmIndexRepository.save(crmIndex));
} catch (JSONException ex) {
throw new RuntimeException("json转换失败:" + ex.getMessage());
}
}
...
}
文件上传的原理与实现可参考该文章《SpringBoot实现微信小程序文件上传的完整案例》
- 测试与验证
- 微信小程序端
– 前端将识别信息与图片上传至服务器 - Redis缓存队列
– 队列中只保存最新10条(数量可自行调整)信息.:
五、Set的应用场景:自动去重
- 创建SpringBoot添加客户信息服务接口;
- 通过Redis集合缓存客户信息,要求自动去重,不得重复记录。
- 客户实体类
/**
* CRM客户信息
*
* @author zhuhuix
*/
@Entity
@Getter
@Setter
@Table(name = "customer")
public class Customer implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull(groups = Update.class)
private Long id;
@Column(name = "open_id")
private String openId;
/**
* 客户代码
*/
@Column(name = "customer_code")
private String customerCode;
/**
* 客户名称
*/
@Column(name = "customer_name")
private String customerName;
/**
* 首字母
*/
@Column(name = "first_letter")
private String firstLetter;
/**
* 创建时间
*/
@Column(name = "create_time")
@CreationTimestamp
private Timestamp createTime;
/**
* 更新时间
*/
@Column(name = "update_time")
@UpdateTimestamp
private Timestamp updateTime;
@Override
public String toString() {
return "Customer{" +
"customerCode='" + customerCode + '\'' +
", customerName='" + customerName + '\'' +
'}';
}
}
- 客户信息WebApi
@ApiOperation(value = "通过扫一扫功能上传客户信息")
@PostMapping(value = "/crmScan/{openId}")
public ResponseEntity crmScan(@RequestBody WxScanDto wxScanDto, @PathVariable String openId) {
return ResponseEntity.ok(wxMiniCrm.wxScan(wxScanDto, openId));
}
- 记录并缓存客户信息实现类
/**
* 微信小程序CRM实现类
*
* @author zhuhuix
*/
@Slf4j
@AllArgsConstructor
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class WxMiniCrmImpl implements WxMiniCrm {
private final UploadFileTool uploadFileTool;
private final CrmIndexRepository crmIndexRepository;
private final CustomerRepository customerRepository;
private final UserService userService;
private final RedisUtils redisUtils;
...
@Override
@Transactional(rollbackFor = Exception.class)
public Result<WxScanDto> wxScan(WxScanDto wxScanDto, String openId) {
//微信扫一扫保存客户信息
if (Constant.SAVE_CUSTOMER_INFO.equals(wxScanDto.getScanType()) && wxScanDto.getJsonObject() != null) {
try {
Customer customer = JSONObject.parseObject(wxScanDto.getJsonObject().toJSONString(), Customer.class);
Customer target = customerRepository.findByCustomerCodeAndOpenId(customer.getCustomerCode(), openId);
if (target != null) {
BeanUtils.copyProperties(customer, target, RepositoryUtil.getNullPropertyNames(customer));
} else {
target = customer;
target.setOpenId(openId);
}
wxScanDto.setReturnObject(customerRepository.save(target));
// 将用户增加的客户信息添加到redis集合中
redisUtils.setAdd(openId.concat("_customer"),customer.toString());
return new Result<WxScanDto>().ok(wxScanDto);
} catch (JSONException ex) {
throw new RuntimeException("json转换失败:" + ex.getMessage());
}
}
return new Result<WxScanDto>().error("无法处理扫一扫功能");
}
}
- 测试与验证
- 相同信息自动去重:通过swagger2进行接口测试,多次提交相同的客户信息
- Redis Desktop Manager验证数据
– 查看集合中的数据,实现自动去重
- 不同集合之间的交集与差集:用户1通过接口添加4个客户
- –用户2通过接口添加3个客户
- – 获取用户1与用户2相同及不同的客户信息
/**
* Redis测试
*
* @author zhuhuix
*/
@SpringBootTest
@Slf4j
public class TestSet {
@Test
void test() {
RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);
//获取交集:相同客户
Set<Object> setInter=redisUtils.setInter("openId1_customer","openId2_customer");
Iterator iterator = setInter.iterator();
log.info("openId1_customer与openId2_customer相同的客户为:");
while(iterator.hasNext()){
log.info(iterator.next().toString());
}
//获取差集:不同客户
Set<Object> setDiff=redisUtils.setDifference("openId1_customer","openId2_customer");
iterator = setDiff.iterator();
log.info("openId1_customer与openId2_customer不同的客户为:");
while(iterator.hasNext()){
log.warn(iterator.next().toString());
}
//获取差集:不同客户
Set<Object> setDiff1=redisUtils.setDifference("openId2_customer","openId1_customer");
iterator = setDiff1.iterator();
log.info("openId2_customer与openId1_customer不同的客户为:");
while(iterator.hasNext()){
log.warn(iterator.next().toString());
}
}
}
- 测试结果