<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123321
# springboot默认使用lecctuce 如果要使用jedis 需要自己引入jedis的依赖
lettuce:
pool:
# 最大连接
max-active: 8
# 最大空闲连接
max-idle: 8
# 最小空闲连接
min-idle: 0
# 最大等待时长,没有等到会报错
max-wait: 100ms
@SpringBootTest
class RedisStringTests {
@Autowired
private RedisTemplate edisTemplate;
@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "虎哥");
// 获取string数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}
// redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 1 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 2 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 3 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 4.1 设置Key的序列化 key和hashkey采用String序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 4.2 设置Value的序列化 value和hashvalue采用JSON序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 5 返回
return template;
}
}
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "虎哥");
// 获取string数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
@Test
void testSaveUser() {
// 写入数据
redisTemplate.opsForValue().set("user:100", new User("虎哥", 21));
// 获取数据
User o = (User) redisTemplate.opsForValue().get("user:100");
System.out.println("o = " + o);
}
}
1、整体可读性有了很大提升
2、能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象
3、但是记录了序列化时对应的class名称,是为了查询时实现自动反序列化,这会带来额外的内存开销
package com.heima;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.redis.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map;
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 存字符串
*/
@Test
void testString() {
// 写入一条String数据
stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
// 获取string数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
// springmvc中默认使用的JSON序列化工具
private static final ObjectMapper mapper = new ObjectMapper();
/**
* 存对象
*/
@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("虎哥", 21);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);
// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1 = " + user1);
}
@Test
void testHash() {
// hash存用的是put 取一个用的是get
stringRedisTemplate.opsForHash().put("user:400", "name", "虎哥");
stringRedisTemplate.opsForHash().put("user:400", "age", "21");
// 一次性把所有的key value都取出来 用entries
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
System.out.println("entries = " + entries);
}
@Test
void name() {
}
}
/**
* 发送手机验证码
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1 校验手机号
if (RegexUtils.isPhoneInvalid(phone)){
// 2 如果返回true 则说明不符合
// 如果不符合 返回错误信息
return Result.fail("手机号格式错误");
}
// 2 生成验证码
String code = RandomUtil.randomNumbers(6);
// 3 保存验证码到session
session.setAttribute("code",code);
// 4 发送验证码
log.debug("成功发送短信验证码,验证码为:"+code);
return Result.ok();
}
/**
* 登录功能
*
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1 校验手机号
// 获取验证码时手机号是正确的 登录时也许会改变
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2 如果不符合 返回错误信息
return Result.fail("手机号格式错误");
}
// 2 校验验证码
// 2.1 用户输入的code
String code = loginForm.getCode();
// 2.1 保存在session中的code
String sessionCode = (String) session.getAttribute("code");
// 这样写太繁琐 换一种方式写
// if (sessionCode == null) {
// return Result.fail("验证码错误");
// } else {
// if (!sessionCode.equals(code)) {
// return Result.fail("验证码错误");
// }
// }
if (sessionCode == null || !sessionCode.equals(code)) {
// 3 验证码不一致 报错
return Result.fail("验证码错误");
}
// 4 验证码不一致 根据手机号查询用户
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone, phone);
User user = baseMapper.selectOne(lqw);
// 5 判断用户是否存在
if (null == user) {
// 6 不存在 创建用户并且保存
user = createUserWithPhone(phone);
}
// 7 保存用户到session
session.setAttribute("user",user);
return Result.ok();
}
/**
* 根据手机号创建用户
* @param phone 手机号
* @return 用户对象
*/
private User createUserWithPhone(String phone) {
// 创建用户
User user = new User();
user.setPhone(phone);
// 创建一个有规律的随机用户名
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
baseMapper.insert(user);
return user;
}
1、每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收
2、每个请求都是独立的,在每个用户去访问工程时,可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
3、在threadLocal中,无论是put方法和get方法, 都是获得当前用户的线程,然后从线程中取出线程的成员变量map
4、只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离
package com.hmdp.interceptor;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1 获取session
HttpSession session = request.getSession();
// 2 获取session中的而用户
UserDTO user = (UserDTO)session.getAttribute("user");
// 3 判断用户是否存在
if (user == null){
// 4 不存在 拦截 返回401状态码
response.setStatus(401);
return false;
}
// 5 存在 保存用户信息到Threadlocal中
UserHolder.saveUser(user);
// 6 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
package com.hmdp.config;
import com.hmdp.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
// 不需要拦截的路径
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/upload/**",
"/shop-type/**",
"/voucher/**"
);
}
}
/**
* 登录校验功能
* @return
*/
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
// 7 保存用户到session
// user中含有全部信息 包括许多敏感信息 需要把user转化成userDTo
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 2 获取session中的而用户
UserDTO user = (UserDTO)session.getAttribute("user");
/**
* 发送手机验证码
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2 如果返回true 则说明不符合
// 如果不符合 返回错误信息
return Result.fail("手机号格式错误");
}
// 3 生成验证码
String code = RandomUtil.randomNumbers(6);
// 4 保存验证码到redis 设置有效期
// 把key设置为常量 有前缀的 login:code
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);
// 5 发送验证码
log.debug("成功发送短信验证码,验证码为:" + code);
return Result.ok();
}
/**
* 登录功能
*
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
// 2 如果不符合 返回错误信息
return Result.fail("手机号格式错误");
}
// 2 校验验证码
// 用户输入的code
String code = loginForm.getCode();
// 从redis获取验证码校验
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (redisCode == null || !redisCode.equals(code)){
// 3 不一致 报错
return Result.fail("验证码错误");
}
// 4 根据手机号查询用户
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone,phone);
User user = baseMapper.selectOne(lqw);
// 5 判断用户是否存在
if (user == null){
// 6 不存在 创建用户并且保存
user=createUserWithPhone(phone);
}
// 7 保存用户信息到redis中
// 7.1 随机生成token 作为登录令牌
// 使用UUID生成token
String token = UUID.randomUUID().toString();
// 7.2 将User对象转为Hash存储
// 将user转变为userDto
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// userDTO转变为Map结构
// Map userMap = BeanUtil.beanToMap(userDTO);
// id是long类型的数据 stringRedisTemplate要求所有数据都必须是string 需要转换成string
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
// 忽略空的值
.setIgnoreNullValue(true)
// 修改字段值 字段名 字段值 -> 修改后的字段名 修改后的字段值
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3 存储 使用Hash结构存储
// 使用putAll:里面有多个key value 需要把userDto转变为Map结构
// 记得设置有效期 session有效期30分钟
// java.lang.Long cannot be cast to java.lang.String
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
// 7.4 设置token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
// 8 返回token
return Result.ok(token);
}
/**
* 根据手机号创建用户
* @param phone 手机号
* @return 用户对象
*/
private User createUserWithPhone(String phone) {
// 创建用户
User user = new User();
user.setPhone(phone);
// 创建一个有规律的随机用户名
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
baseMapper.insert(user);
return user;
}
// 这里是拦截器 不在spring容器中 不能同@Autowired注入stringRedisTemplate
// 换个方法 在配置类中注入
// 或者加个注解:@Configuration 就可以自动注入啦
private StringRedisTemplate stringRedisTemplate;
// 构造函数
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1 获取请求头中的token
// 这一步需要前端把token保存到请求头中
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
// 不存在 拦截 返回401状态码
response.setStatus(401);
return false;
}
// 2 基于token获取redis中的用户
// 只用 get 只能取出一个
// 用entrise 取出的是一个map
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token);
// 3 判断用户是否存在
// 如果userMap是null的话 自动返回一个空值
if (userMap.isEmpty()){
return true;
}
// 5 将查询到的Map对象转为UserDto
// 忽略转换过程的错误:肯定不忽略
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);
// 6 存在 保存用户信息到Threadlocal中
UserHolder.saveUser(userDTO);
// 7 刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8 放行
return true;
}
// @Configuration:这里可以注入
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
// 不需要拦截的路径
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/upload/**",
"/shop-type/**",
"/voucher/**"
);
}
package com.hmdp.interceptor;
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import io.netty.util.internal.StringUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 这里是拦截器 不在spring容器中 不能同@Autowired注入stringRedisTemplate
// 换个方法 在配置类中注入
// 或者加个注解:@Configuration 就可以自动注入啦
private StringRedisTemplate stringRedisTemplate;
// 添加一个构造器
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1 获取token(请求头中的)
// 这一步需要前端把token保存到请求头中
String token = request.getHeader("authorization");
if(StringUtil.isNullOrEmpty(token)){
// 不需要拦截 返回true 直接放行 后面还有一个拦截器
return true;
}
// 2 基于token获取redis中的用户
// 只用 get 只能取出一个
// 用entrise 取出的是一个map
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token);
// 3 判断用户是否存在
if (userMap.isEmpty()){
// 不需要拦截 返回true 直接放行 后面还有一个拦截器
return true;
}
// 5 将查询到的Map对象转为UserDto
// 忽略转换过程的错误:肯定不忽略
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);
// 6 存在 保存用户信息到Threadlocal中
UserHolder.saveUser(userDTO);
// 7 刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
package com.hmdp.interceptor;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import io.netty.util.internal.StringUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
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);
// 拦截
return false;
}
// 有用户 放行
return true;
}
}
package com.hmdp.config;
import com.hmdp.interceptor.LoginInterceptor;
import com.hmdp.interceptor.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
// @Configuration:这里可以注入
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 后执行 默认情况下按照添加顺序执行 拦截需要登录的请求
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/upload/**",
"/shop-type/**",
"/voucher/**"
)
// 确定执行顺序 越小越先执行
.order(1);
// 先执行 拦截所有请求
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**")
// 确定执行顺序 越小越先执行
.order(0);
}
}
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// key要唯一 就用id
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4 shop不存在 根据id查询数据库
Shop shopById = iShopService.getById(id);
// 5 不存在 返回错误
if (shopById == null){
return Result.fail("该商铺不存在");
}
// 6 存在 写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById));
// 7 返回
return Result.ok(shopById);
}
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新(选择这个)
先删除缓存,再操作数据库
先操作数据库,再删除缓存(选择这个)
// 6 存在 写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById)
,CACHE_SHOP_TTL, TimeUnit.MINUTES);
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
package com.hmdp.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
*
* 服务实现类
*
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IShopService iShopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// key要唯一 就用id
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断是否是空值 是空值的话 就说明店铺不存在
if (shopJson == ""){
// 返回一个错误信息
return Result.fail("该店铺不存在");
}
// 4 shop不存在 根据id查询数据库 shopJson == null
Shop shopById = iShopService.getById(id);
// 5 不存在 返回错误
if (shopById == null){
// 5.1 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
// 5.2 返回错误信息
return Result.fail("该商铺不存在");
}
// 6 存在 写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById)
,CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7 返回
return Result.ok(shopById);
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
}
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性(微服务)
给缓存业务添加降级限流策略(微服务)
给业务添加多级缓存(微服务)
package com.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
*
* 服务实现类
*
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IShopService iShopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queyWithPassThrough(id);
// 互斥锁解决缓存击穿
Shop shop = queyWithMutex(id);
// 使用逻辑过期解决缓存击穿
// 7 返回
if (shop == null){
return Result.fail("该店铺不存在");
}
return Result.ok(shop);
}
// 线程池
private static final ExecutorService CACHE_RREBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queyWithLogicalExpie(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isBlank(shopJson)) {
// 3 不存在直接返回 JSON格式变回类对象
return null;
}
// 4 shopJson不为空 则需要把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
// 先强行转化为JSONObject
JSONObject data = (JSONObject)redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5 判断逻辑时间是否过期
if(expireTime.isAfter(LocalDateTime.now())){
// 6 未过期 则直接返回商铺信息
return shop;
}
// 7 过期 需要重建缓存
// 7.1 尝试获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean flag = tryLock(lockKey);
if (flag){
// 7.3 获取互斥锁成功 开启独立线程 实现缓存重建
CACHE_RREBUILD_EXECUTOR.submit(()->{
try {
// 重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
// 7.2 获取互斥锁失败 返回店铺信息
// 不管成功还是失败 最后都是要返回shop
return shop;
}
/**
* 存储逻辑过期时间
*
* @param id
*/
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 1 查询商铺信息
Shop shop = iShopService.getById(id);
Thread.sleep(200);
// 2 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3 写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
/**
* 互斥锁解决缓存击穿
*
* @param id
* @return
*/
public Shop queyWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 是一个空值
if (shopJson.equals("")) {
return null;
}
// 既没有数据 也没有空值 是null
// 4 实现缓存重建
// 4.1 获取互斥锁
// 锁的key和缓存的key不一样
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
boolean flag = tryLock(lockKey);
// 4.2 判断是否获取成功
if (!flag) {
// 4.3 失败 则休眠并且重试
Thread.sleep(50);
// 进行递归 要返回
return queyWithMutex(id);
}
// 4.4 成功 根据id查询数据库
shop = iShopService.getById(id);
// 模拟重建缓存 在本地查询太快了 休眠一下
Thread.sleep(200);
// 查询数据库结果 不存在 返回错误
if (shop == null) {
// 将空值写入redis 这里写入的是空值 而不是Null
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 4.5 将商铺数据写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop)
, CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 5 最终必须释放互斥锁
unLock(lockKey);
}
// 6 返回数据
return shop;
}
/**
* 获取锁
*/
public boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 不能直接返回 有可能会出现空指针
// 上面是引用类型 转换为基本类型
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
public void unLock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 缓存穿透代码
* 返回空或者数据本身
* @param id
* @return
*/
public Shop queyWithPassThrough(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1 从redis查询商铺缓存 以店铺ID为key
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2 判断是否存在
// null "" "\t\n" 都会被认为是false
if (StrUtil.isNotBlank(shopJson)) {
// 3 存在直接返回 JSON格式变回类对象
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 是一个空值
if (shopJson.equals("")) {
return null;
}
// 4 不存在 根据id查询数据库
Shop shopById = iShopService.getById(id);
// 5 查询数据库结果 不存在 返回错误
if (shopById == null) {
// 将空值写入redis 这里写入的是空值 而不是Null
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6 存在 写入redis 设置过期时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById)
, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7 返回
return shopById;
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@Override
// 保证原子性
@Transactional
public Result updateShop(Shop shop) {
// 获取店铺id
Long id = shop.getId();
if (id == null){
return Result.fail("店铺ID不能为空");
}
// 1 更新数据库
iShopService.updateById(shop);
// 2 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
}
package com.hmdp.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
// 定义一个初始时间戳
public static final long BEGIN_TIME = 1640995200L;
// 序列号位数
public static final long COUNT_BITS = 32;
// 用到redis的自增长
@Autowired
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix){
// 1 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIME;
// 2 生成序列号
// 确定当天序列号的key 获取当天日期 精确到天
String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:DD"));
// 实现自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data);
// 3 拼接并返回 借助位运算
// 移位之后后面32位全都是0 或运算可以保证原来的样子
return timeStamp << COUNT_BITS | count;
}
public static void main(String[] args) {
// LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
// long timeToSecond = time.toEpochSecond(ZoneOffset.UTC);
// System.out.println(timeToSecond);
}
}
/**
* 新增普通券
* @param voucher 优惠券信息
* @return 优惠券id
*/
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {
voucherService.save(voucher);
return Result.ok(voucher.getId());
}
/**
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
baseMapper.insert(voucher);
// 保存秒杀信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
}
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
*
* 服务实现类
*
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisIdWorker redisIdWorker;
/**
* 优惠券秒杀功能
* @param voucherId
* @return
*/
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1 根据id查询优惠券信息
LambdaQueryWrapper<SeckillVoucher> lqw = new LambdaQueryWrapper<>();
lqw.eq(SeckillVoucher::getVoucherId,voucherId);
SeckillVoucher seckillVoucher = seckillVoucherService.getOne(lqw);
// 2 判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀尚未开始");
}
// 3 判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀已经结束");
}
// 4 判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("秒杀券已经不足");
}
// 5 扣减库存
seckillVoucher.setStock(seckillVoucher.getStock()-1);
boolean success = seckillVoucherService.update(seckillVoucher, null);
if (!success){
return Result.fail("秒杀券已经不足");
}
// 6 创建订单 写入数据库
VoucherOrder voucherOrder = new VoucherOrder();
// 生成全局唯一Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
baseMapper.insert(voucherOrder);
// 7 返回订单id
return Result.ok(orderId);
}
}
// 4 判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("秒杀券已经不足");
}
// 5 扣减库存
seckillVoucher.setStock(seckillVoucher.getStock()-1);
boolean success = seckillVoucherService.update(seckillVoucher, null);
if (!success){
return Result.fail("秒杀券已经不足");
}