1 初识Redis
Redis(Remote Dictionary Server,远程词典服务器)是一个基于内存的健值型数据库,特征如下:
1、健值型,value支持多种不同数据类型
2、单线程,每个命令具备原子性
3、低延迟、速度快(基于内存、IO多路复用、良好的编码),基于C语言实现
4、支持数据持久化
5、支持主从集群、分片集群
6、支持多语言客户端
redis-server 启动Redis
ps -ef | grep redis 查看Redis进程
redis-cli -h 127.0.0.1 -p 6379 连接redis服务
Redis中数据类型,Key一般是String类型,Value包含以下类型:
GEO:地理坐标
2 Redis命令
2.1 Redis通用命令
KEYS:查看所有符合指定模版的key。搜索效率低,生产环境不要用该命令。
查询所有的key keys *
查询所有以a开头的key keys a*
DEL:删除指定的key,可以一个或者多个key。返回值是删除了多少个key。
Exists:判断key是否存在
expire:设置key的有效期,单位是秒,有效期过后自动删除。不设置有效时间的话,默认永久有效(返回-1)
ttl:查看一个key的剩余有效期
2.2 string类型常用命令
set:添加或者修改已经存在的一个string类型的键值对
get:根据key获取string类型的value
mset:批量添加多个string类型的键值对
mget:批量获取多个key的value
Incr:让一个整型key自增1
Incrby:让一个整型的key自增并指定步长,例如incrby num 2 让num值自增2
Incrbyfloat:让一个浮点型数字自增并指定步长
setnx:添加一个string类型的键值对,前提是这个key不存在,否则不执行
setex:添加一个string类型的键值对,并指定有效期。
setex name2 10 jack等同于set name2 jack ex 10
实际项目中key要用多个单词形成层级结构,多个单词之间用:隔开,格式如下:
项目名:业务名:类型:id。如果Value是一个对象,可以将对象序列化为JSON字符串后存储
2.3 hash类型常用命令
hset key field value:添加或者修改hash类型key的field的值
hget key field:获取一个hash类型key的field的值
hmset:批量添加多个hash类型key的field的值
hmget:批量获取多个hash类型key的field的值
HgetAll:获取一个hash类型的key中的所有的field和value
Hkeys:获取一个hash类型的key中的所有的field
hvals:获取一个hash类型的key中所有的value
Hincrby:让一个hash类型key的字段值自增并指定步长
hsetnx:添加一个hash类型的key的filed值,前提是这个field不存在,否则不执行。
2.4 list类型常用命令
Redis中的List类型可以看做是一个双向链表结构,既可以支持正向检索也可以支持反向检索。List中的元素有序排列、元素可以重复,插入和删除速度快、查询速度一般。
2.5 set类型常用命令
Redis中的Set结构可以看做是一个value为null的HashMap,底层也是Hash表结构,特点如下:
1、元素无序
2、元素不可重复
3、查找速度快
4、支持交集、并集、差集等功能
单个set操作:
多个set间互相操作:
2.6 SortedSet类型常用命令
Redis中的SortedSet是一个可排序的set集合,SortedSet中每一个元素都带有
一个score属性,可以基于score属性对元素排序,底层实现是一个跳表(SkipList)加Hash表,常用来实现排行榜功能。特征如下:
1、可排序,每个元素都带score属性,根据score属性排序
2、元素不可重复
3、查询速度快
3 Redis的Java客户端
3.1 Jedis
官网地址:https://github.com/redis/jedis
引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.0</version>
</dependency>
基本用法:
//Jedis中的方法名和Redis命令行命令名相同
Jedis jedis = new Jedis("127.0.0.1",6379);//建立连接
@Test
public void testJedis() {
jedis.select(0);//选择数据库
System.out.println( jedis.get("age"));//取数据
jedis.setnx("school","bupt");//存数据
if (jedis!=null) {
jedis.close();//释放资源
}
}
3.1.1 Jedis连接池
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//最大连接数
jedisPoolConfig.setMaxTotal(10);
//最大空闲连接,超过一段时间后空闲连接会被撤销
jedisPoolConfig.setMaxIdle(10);
//最小空闲连接
jedisPoolConfig.setMinIdle(2);
//设置最长等待时间,毫秒
jedisPoolConfig.setMaxWaitMillis(200);
jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 1000);
}
/**
* 获取jedis对象
* @return
*/
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
3.2 SpringDataRedis
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
引入依赖:
<!-- Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
yaml配置文件:
spring:
data:
redis:
host: 127.0.0.1
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100
注入RedisTemplate:
@SpringBootTest
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
void testString() {
redisTemplate.opsForValue().set("salary",500000);//存储数据
System.out.println( redisTemplate.opsForValue().get("salary"));//查询数据
}
}
3.2.1 RedisTemplate
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同类型中:
4 Redis实战案例
4.1 基于redis实现短信登录
发送短信验证码
用户输入手机号后,点击“发送验证码”,请求api/user/code?phone=13456781234接口,将手机号码发送到服务端,code接口response中不需要携带具体数据。服务端生成验证码,保存验证码到session中,然后向指定的手机号发送验证码。
请求方式:post
请求路径:user/code
请求参数:phone,手机号码
返回值:无
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
}
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
}
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码;生成6位随机数字
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.调用验证码服务,发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List<?> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
短信验证码登录、注册
用户输入验证码后,点击“登录”请求/user/login接口,将手机号和验证码放到body中。
请求方式:post
请求路径:user/login
请求参数:phone和code
返回值:无
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码。RequestBody注解接收请求体中的phone、code、password参数
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
}
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
Result login(LoginFormDTO loginForm, HttpSession session);
}
import lombok.Data;
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码;生成6位随机数字
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到 redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.调用验证码服务,发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ? 使用myBatisPlus实现查询数据库
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在;数据库中存在该手机号的话,说明是已经注册的用户;数据库中没有该手机号的话,说明用户没有注册过。
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到 redis中
// 7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
// 1.创建用户,保存手机号和昵称,其他字段先不管,以后用户可以更新资料
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));//昵称随机生成
// 2.保存用户,使用myBatisPlus入库
save(user);
return user;
}
}
3 使用拦截器实现登录校验
用户的所有请求先经过拦截器,在拦截器中判断用户的登录状态,然后再请求具体的服务。
//配置拦截器
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截,设置状态码
response.setStatus(401);//401表示未授权
// 拦截
return false;
}
// 有用户,则放行
return true;
}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
//工具类,封装ThreadLocal
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
//配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器,只拦截需要登录的请求
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// 拦截所有请求,token刷新的拦截器,目的在于刷新token的有效期;RefreshTokenInterceptor拦截器优先级高,先执行;LoginInterceptor优先级低,后执行;拦截器优先级通过order控制
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
//获取当前登录的用户并返回
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
}
每个Tomcat服务有自己的session存储空间,多个Tomcat服务之间session存储空间隔离,session无法共享。多个Tomcat服务可以访问Redis内存空间。
4.2 商户查询缓存
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
}
public interface IShopService extends IService<Shop> {
Result queryById(Long id);
}
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
// 解决缓存穿透
Shop shop = cacheClient
.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 互斥锁解决缓存击穿
// Shop shop = cacheClient
// .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 逻辑过期解决缓存击穿
// Shop shop = cacheClient
// .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商铺名称
*/
private String name;
/**
* 商铺类型的id
*/
private Long typeId;
/**
* 商铺图片,多个图片以','隔开
*/
private String images;
/**
* 商圈,例如陆家嘴
*/
private String area;
/**
* 地址
*/
private String address;
/**
* 经度
*/
private Double x;
/**
* 维度
*/
private Double y;
/**
* 均价,取整数
*/
private Long avgPrice;
/**
* 销量
*/
private Integer sold;
/**
* 评论数量
*/
private Integer comments;
/**
* 评分,1~5分,乘10保存,避免小数
*/
private Integer score;
/**
* 营业时间,例如 10:00-22:00
*/
private String openHours;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
@TableField(exist = false)
private Double distance;
}