redis是什么
redis是一种NoSQL数据库,其具备如下特性:
1. 基于内存运行,性能高效;
2. 支持分布式,理论上可无限扩展;
3. key-value存储系统;
4. 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、key-value数据库,并提供多种语言的API
Spring为什么要使用redis
缓存热点数据,对热点数据进行排序等
redis在Spring中的应用
- Spring Boot中配置和使用Redis
application.yml
# 配置redis连接池属性
spring:
redis:
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000ms
port: 6379
host: 10.xx.xx.xx
timeout: 1000ms
# 缓存管理器配置
cache:
cache-names: redisCache
type: REDIS
# redis:
# cache-null-values: true #是否允许Redis缓存空值
# key-prefix: # Redis的键前缀
# time-to-live: 0ms # 缓存超时时间戳,配置为0则不设置超时时间
# use-key-prefix: true # 是否启用Redis的键前缀
#
spring:
datasource:
name: ceph_manager
# 坚炼测试机
url: jdbc:mysql://10.xx.xx.xx:3306/ys?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&rewriteBatchedStatements=true&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
#druid相关配置
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
## mybatis配置
mybatis:
mapper-locations: classpath:mapping/*.xml #注意:一定要对应mapper映射xml文件的所在路径
type-aliases-package: com.example.springbootlocal.model # 注意:对应实体类的路径
## 日志配置为DEBUG级别,这样日志最为详细
logging:
level:
root: DEBUG
org:
springframework: DEBUG
org:
mybatis: DEBUG
Application.java
package com.example.springbootlocal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import javax.annotation.PostConstruct;
@EnableCaching
@SpringBootApplication(scanBasePackages = "com.example.springbootlocal")
public class SpringbootlocalApplication {
@Autowired
private RedisTemplate redisTemplate;
/**
* 利用Spring Bean声明周期的特性使用注解@PostConstruct自定义后初始化方法。
*/
@PostConstruct
public void init() {
initRedisTemplate();
}
/**
* 在RedisTemplate中它会默认地定义一个StringRedisSerializer对象,所以这里并没有自己创建一个新的StringRedisSerializer对象,
* 而是从RedisTemplate中获取。然后把RedisTemplate关于键和其散列数据类型的filed都修改为了使用StringRedisSerializer进行
* 序列化,这样在Redis服务器上得到的键和散列的field就都以字符串存储了。
*/
public void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootlocalApplication.class, args);
}
}
- redis基本使用
package com.example.springbootlocal.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/stringAndHash")
public Map<String, Object> testStringAndHash() {
redisTemplate.opsForValue().set("key1", "value1");
// 注意这里使用了JDK的序列化器,所以Redis保存时不是整数,不能运算
redisTemplate.opsForValue().set("int_key", "1");
stringRedisTemplate.opsForValue().set("int", "1");
// 使用运算
stringRedisTemplate.opsForValue().increment("int", 1);
// 获取底层Jedis连接
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
// 减1操作,这个命令RedisTemplate不支持,所以先获取底层的连接操作
jedis.decr("int");
Map<String, String> hash = new HashMap<>();
hash.put("field1", "value1");
hash.put("field2", "value2");
// 存入一个散列数据类型
stringRedisTemplate.opsForHash().putAll("hash", hash);
// 新增一个字段
stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
// 绑定散列操作的key,这样可以连续对同一个散列数据类型进行操作
BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
// 删除两个字段
hashOps.delete("field1", "field2");
// 新增一个字段
hashOps.put("field4", "value5");
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/list")
public Map<String, Object> testList() {
// 插入两个列表,注意它们在链表的顺序
// 链表从左到右顺序为v10, v8, v6, v4, v2
stringRedisTemplate.opsForList().leftPushAll(
"list", "v2", "v4", "v6", "v8", "v10"
);
// 链表从左到右顺序为v1, v2, v3, v4, v5, v6
stringRedisTemplate.opsForList().rightPushAll(
"list2", "v1", "v2", "v3", "v4", "v5", "v6"
);
// 绑定list2链表操作
BoundListOperations<String, String> listOps = stringRedisTemplate.boundListOps("list2");
// 从右边弹出一个成员
Object result1 = listOps.rightPop();
// 获取定位元素,Redis从0开始计算,这里值为v2
Object result2 = listOps.index(1);
// 从左边插入链表
listOps.leftPush("v0");
// 求链表长度
Long size = listOps.size();
// 求链表下标区间成员,整个链表下标范围为0到size-1,这里不取最后一个元素
List elements = listOps.range(0, size - 2);
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/set")
public Map<String, Object> testSet() {
// 注意:这里v1重复两次,因为集合不允许重复,所以只是插入5个成员到集合中
stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
// 绑定set1集合操作
BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
// 增加两个元素
setOps.add("v6", "v7");
//删除两个元素
setOps.remove("v1", "v7");
// 返回所有元素
Set set1 = setOps.members();
// 求成员数
Long size = setOps.size();
// 求交集
Set inter = setOps.intersect("set2");
// 求交集,并且用新集合inter保存
setOps.intersectAndStore("set2", "inter");
// 求差集
Set diff = setOps.diff("set2");
// 求差集,并且用新集合diff保存
setOps.diffAndStore("set2", "diff");
// 求并集
Set union = setOps.union("set2");
// 求并集,并且用新集合union保存
setOps.unionAndStore("set2", "union");
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
/**
* 一些网站中,经常会有排名,如最热门的商品或者最大的购买买家,都是常常见到的场景
* 对于这类排名,刷新往往需要及时,也涉及较大的统计,如果使用数据库会太慢。
* 为了支持集合的排序,Redis还提供了有序集合(zset)。有序集合与集合的差异并不大,它也是
* 一种散列表存储方式,同时它的有序性只是靠它在数据结构中增加一个属性——score(分数)得以支持
* @return
*/
@RequestMapping("/zset")
public Map<String, Object> testZset() {
Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
for (int i = 1; i <= 9; i++) {
// 分数
double score = i*0.1;
// 创建一个TypedTuple对象,存入值和分数
ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<>("value" + i, score);
typedTupleSet.add(typedTuple);
}
// 往有序集合插入元素
stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
// 绑定zset1有序集合操作
BoundZSetOperations<String, String> zSetOps = stringRedisTemplate.boundZSetOps("zset1");
// 增加一个元素
zSetOps.add("value10", 0.26);
Set<String> setRange = zSetOps.range(1, 6);
// 按分数排序获取有序集合
Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
// 定义值范围
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gt("value3"); // 大于value3
// range.gte("value3"); // 大于等于value3
// range.lt("value8"); // 小于value8
range.lte("value8"); // 小于等于value8
// 按值排序,注意这个排序是按字符串排序
Set<String> setLex = zSetOps.rangeByLex(range);
// 删除排序
zSetOps.remove("value9", "value2");
// 求分数
Double score = zSetOps.score("value8");
// 在下标区间下,按分数排序,同时返回value和score
Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOps.rangeWithScores(1, 6);
// 在分数区间下,按分数排序,同时返回value和score
Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOps.rangeByScoreWithScores(1, 6);
// 按从大到小排序
Set<String> reverseSet = zSetOps.reverseRange(2, 8);
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
}
- 使用Redis事务
在Redis中使用事务,通常的命令组合是watch…multi…exec,即要在一个Redis连接中执行多个命令,这时就可以考虑用SessionCallback接口来达到这个目的。
watch命令是可以监控Redis的一些键;
multi命令是开始事务,开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列里,所以此时调用Redis命令,结果都是返回null;
exe命令的意义在于执行事务,只是它在队列命令执行前会判断被watch监控的Redis的键的数据是否发生过变化(即使赋予与之前相同的值也会被认为是变化过),如果它认为发生了变化,则Redis就会取消事务,否则就会执行事务
/**
* 使用Redis事务
* @return
*/
@RequestMapping("/multi")
public Map<String, Object> testMulti() {
redisTemplate.opsForValue().set("key1", "value1");
List list = (List)redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
// 设置要监控key1
redisOperations.watch("key1");
// 开启事务,在exec命令执行前,全部都只是进入队列
redisOperations.multi();
redisOperations.opsForValue().set("key2", "value2");
//redisOperations.opsForValue().increment("key1", 1);
// 获取值将为null,因为redis只是把命令放入队列
Object value2 = redisOperations.opsForValue().get("key2");
System.out.println("命令在队列,所以value为null【" + value2 + "】");
redisOperations.opsForValue().set("key3", "value3");
Object value3 = redisOperations.opsForValue().get("key3");
System.out.println("命令在队列,所以value为null【" + value3 + "】");
// 执行exec命令,将先判别key1是否在监控后被修改过,如果是则不执行事务,否则就执行事务
return redisOperations.exec();
}
});
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
- 使用Redis流水线
/**
* 使用Redis流水线:
* 默认情况下,Redis客户端是一条条命令发送给Redis服务器的,这样显然性能不高,
* Redis也可以像关系数据库中批量执行命令,即流水线(pipeline)技术
* @return
*/
@RequestMapping("/pipeline")
public Map<String, Object> testPipeline() {
long start = System.currentTimeMillis();
List list = redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
for (int i = 1; i <= 100000; i++) {
redisOperations.opsForValue().set("pipeline_" + i, "value_" + i);
String value = (String) redisOperations.opsForValue().get("pipeline_" + i);
if (i == 100000) {
System.out.println("命令只是进入队列,所以值为空【" + value + "】");
}
}
return null;
}
});
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "毫秒。");
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
- 使用Redis发布订阅
- Spring缓存实践——使用Spring缓存注解操作Redis
POJO类——User.java 对应数据库的表
package com.example.springbootlocal.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
@ApiModel(description = "用户实体")
public class User implements Serializable {
private static final long serialVersionUID = 77732723737327233L;
@ApiModelProperty("用户编号")
private Long id;
@NotNull
@ApiModelProperty("用户姓名")
private String userName;
@ApiModelProperty("note")
private String note;
@NotNull
@ApiModelProperty("用户年龄")
private int age;
}
MyBatis用户操作接口: UserDao.java
package com.example.springbootlocal.dao;
import com.example.springbootlocal.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.data.repository.query.Param;
import java.util.List;
@Mapper
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
int updateUser(User user);
List<User> findUsers(@Param("userName") String userName,
@Param("note") String note);
int deleteUser(Long id);
}
定义用户SQL和映射关系: UserDaoMapping.xml 这里需要注意通过将属性useGeneratedKeys设置为true,代表将通过数据库生成主键,而将keyProperty设置为POJO的id属性,MyBatis就会将数据库生成的主键回填到POJO的id属性中
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.springbootlocal.dao.UserDao" >
<resultMap id="user" type="com.example.springbootlocal.model.User">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="userName" property="userName" jdbcType="VARCHAR" />
<result column="age" property="age" jdbcType="INTEGER" />
<result column="note" property="note" jdbcType="VARCHAR" />
</resultMap>
<sql id="Base_Column_List" >
id, userName, age, note
</sql>
<select id="getUser" parameterType="long" resultType="com.example.springbootlocal.model.User">
SELECT id, userName, note age
FROM ys_user
WHERE id = #{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="com.example.springbootlocal.model.User">
INSERT INTO ys_user(userName, age, note)
VALUES (#{userName}, #{age}, #{note})
</insert>
<update id="updateUser">
UPDATE ys_user
<set>
<if test="userName != NULL">
userName = #{userName},
</if>
<if test="age != NULL">
age = #{age},
</if>
<if test="note != NULL">
note = #{note}
</if>
</set>
WHERE id = #{id}
</update>
<select id="findUsers" resultType="com.example.springbootlocal.model.User">
SELECT <include refid="Base_Column_List" />
FROM ys_user
<where>
<if test="userName != NULL">
userName = #{userName},
</if>
<if test="note != NULL">
note = #{note}
</if>
</where>
</select>
<delete id="deleteUser" parameterType="long">
DELETE FROM ys_user
WHERE id = #{id}
</delete>
</mapper>
Spring MVC中的servicer层的业务处理接口:UserService.java
package com.example.springbootlocal.service;
import com.example.springbootlocal.model.User;
import java.util.List;
public interface UserService {
User getUser(Long id);
User insertUser(User user);
User updateUserName(Long id, String userName);
List<User> findUsers(String userName, String note);
int deleteUser(Long id);
}
service层实现——UserServiceImpl.java
package com.example.springbootlocal.serviceImpl;
import com.example.springbootlocal.dao.UserDao;
import com.example.springbootlocal.model.User;
import com.example.springbootlocal.service.UserService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
/**
* 插入用户,最后MyBatis会回填id,取结果id缓存用户
* @param user
* @return
*/
@Override
@Transactional
@CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
public User insertUser(User user) {
userDao.insertUser(user);
return user;
}
/**
* 获取id,取参数id缓存用户
* @param id
* @return
*/
@Override
@Transactional
@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
public User getUser(Long id) {
return userDao.getUser(id);
}
/**
* 更新数据后,更新缓存,如果condition配置项使结果返回为null,不缓存
* @param id
* @param userName
* @return
*/
@Override
@Transactional
@CachePut(value = "redisCache", condition = "#result != 'null'", key = "'redis_user_'+#id")
public User updateUserName(Long id, String userName) {
User user = this.getUser(id);
if (user == null) {
return null;
}
user.setUserName(userName);
userDao.updateUser(user);
return user;
}
/**
* 命中率低,所以不采用缓存机制
* @param userName
* @param note
* @return
*/
@Override
@Transactional
public List<User> findUsers(String userName, String note) {
return userDao.findUsers(userName, note);
}
/**
* 移除缓存
* @param id
* @return
*/
@Override
@Transactional
@CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
public int deleteUser(Long id) {
return userDao.deleteUser(id);
}
}
下面通过以接口的形式来测试缓存注解:UserController.java
package com.example.springbootlocal.controller;
import com.example.springbootlocal.model.User;
import com.example.springbootlocal.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/users")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/getUser")
public User getUser(Long id) {
return userService.getUser(id);
}
@RequestMapping("/insertUser")
public User insertUser(String userName, String note) {
User user = new User();
user.setUserName(userName);
user.setNote(note);
userService.insertUser(user);
return user;
}
@RequestMapping("/findUsers")
public List<User> findUsers(String userName, String note) {
return userService.findUsers(userName, note);
}
@RequestMapping("/updateUserName")
public Map<String, Object> updateUserName(Long id, String userName) {
User user = userService.updateUserName(id, userName);
boolean flag = user != null;
String message = flag ? "更新成功" : "更新失败";
return resultMap(flag, message);
}
@RequestMapping("/deleteUser")
public Map<String, Object> deleteUser(Long id) {
int result = userService.deleteUser(id);
boolean flag = result == 1;
String message = flag ? "删除成功" : "删除失败";
return resultMap(flag, message);
}
private Map<String, Object> resultMap(boolean success, String message) {
Map<String, Object> result = new HashMap<String, Object>();
result.put("success", success);
result.put("message", message);
return result;
}
}
@CachePut表示将方法结果返回存放到缓存中;
@Cacheable表示先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行该方法,返回数据,并且将返回结果保存到缓存中;
@CacheEvict通过定义的键移除缓存,它有一个Boolean类型的配置项beforeInvocation,表示在方法之前或者之后移除缓存。因为其默认值为false,所以默认为方法之后将缓存移除。
- 自定义缓存管理器
缓存注解自调用失效问题:
正如上面描述的问题所说,我们并不希望采用Spring Boot机制带来的键命名方式,也不希望缓存永不超时,这时我们可以自定义缓存管理器。
有两种方法定制缓存管理器:
- 通过配置消除缓存键的前缀和自定义超时时间的属性来定制生成RedisCacheManager;
- 另一种是不采用Spring Boot为我们生成的方式,而是完全通过自己的代码创建缓存管理器,尤其是当需要比较多自定义的时候,更加推荐你采用自定义的代码;
附录
- 参考文章
《深入浅出Spring Boot2.x》