进入redis安装目录
cd /usr/local/src/redis-6.2.6
启动
redis-server redis.conf
一.数据结构
1.String
2.Hash
3.List
4.Set
5.SortedSet
二.Jedis
1.基本使用
2.连接池
3.SpringDataRedis
3.1基本使用
3.2序列化
如果mvc没自带,需要导入jackson依赖
- 序列化的类型
3.3序列化方式二:StringRedisTemplate
三.短信登录
1.发送短信验证码
1.1.RegexUtils校验手机号的正则表达式
package com.hmdp.utils;
public abstract class RegexPatterns {
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}
1.2.实现
UserService
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.保存验证码
session.setAttribute("code",code);
//4.短信发送验证码(未开发)
log.debug(code);
return Result.ok();
}
UserController
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// TODO 发送短信验证码并保存验证码
return userService.sendCode(phone,session);
}
2.短信登录和注册
UserController
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
return userService.login(loginForm,session);
}
Service
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//2.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (cacheCode==null || !cacheCode.toString().equals(code)) {
return Result.fail("验证码错误");
}
//3.根据手机号查询用户
User user = query().eq("phone", phone).one();
//4.用户是否存在
if (user == null){
user = createUserWithPhone(phone);
}
//5.保存用户信息刀session
session.setAttribute("User",user);
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+ phone);
save(user);
return user;
}
优雅常量写法
package com.hmdp.utils;
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "D:\\lesson\\nginx-1.18.0\\html\\hmdp\\imgs\\";
public static final String USER_NICK_NAME_PREFIX = "user_";
public static final int DEFAULT_PAGE_SIZE = 5;
public static final int MAX_PAGE_SIZE = 10;
}
3.登录校验拦截
3.1.拦截器
LoginInterceptor.java
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
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用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if (user == null){
//4.不存在拦截
response.setStatus(401);
return false;
}
//5.存在保存刀threadLocal
UserHolder.saveUser((UserDTO) user);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
MvcConfig.java
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
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/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
3.2.封装ThredLocal工具类
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
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();
}
}
4.Redis代替Session
4.1发送短信验证码
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.保存验证码到redis
// session.setAttribute("code",code);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//key,value,过期时间,单位
//4.短信发送验证码(未开发)
log.debug("==============="+code+"===============");
return Result.ok();
}
4.2.短信验证和登录
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;
@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)){
return Result.fail("手机格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//TODO 3.保存验证码到redis
// session.setAttribute("code",code);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//key,value,过期时间,单位
//4.短信发送验证码(未开发)
log.debug("==============="+code+"===============");
return Result.ok();
}
/*
* 登录*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//TODO 2. 校验验证码
// Object cacheCode = session.getAttribute("code");
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode==null || !cacheCode.equals(code)) {
return Result.fail("验证码错误");
}
//3.根据手机号查询用户
User user = query().eq("phone", phone).one();
//4.用户是否存在
if (user == null){
user = createUserWithPhone(phone);
}
//TODO 5.保存用户信息刀redis
//5.1生成token
String token = UUID.randomUUID().toString(true);
//5.2将User转换为hash
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
new CopyOptions()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
//map的所以字段都转换成string,否则存redis会报错
//5.4存储
String tokenKey = LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//设置有效时间
//5.2将user对象
return Result.ok(token);
}
/*
* 插入一条数据*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+ phone);
save(user);
return user;
}
}
4.3 拦截器
LoginInterceptor.java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
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;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
//通过构造器注入stringRedisTemplate
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TODO 1.获取Token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
response.setStatus(401);
return false;
}
//TODO 2.获取Token用户
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
//TODO 3.判断用户是否存在
if (userMap.isEmpty()){
//4.不存在拦截
response.setStatus(401);
return false;
}
//TODO 5.将map转user
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//TODO 6.存在保存到threadLocal
UserHolder.saveUser(userDTO);
//TODO 7.刷新token有效期
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//TODO 6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
MvcConfig.java
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
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;
import javax.annotation.Resource;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
四.商户查询缓存
1.添加商户缓存
Json转bean
SONUtil.toBean(shopJson, Shop.class);
bean转JsonJSONUtil.toJsonStr(shop);
Json转list
JSONUtil.parseArray(shopTypeJson).toList(ShopType.class);
list转JsonJSONUtil.toJsonStr(typeList);
//商户缓存
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String cacheShop = "cache:shop:";
//1.从redis中查询
String shopJson = stringRedisTemplate.opsForValue().get(cacheShop + id);
//2.数据存在redis直接返回
if (StrUtil.isNotBlank(shopJson)){
//Json转bean
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//3.不存在查询数据库
Shop shop = getById(id);
//判断数据库中是否存在
if(shop == null){
return Result.fail("店铺不存在");
}
//Bean转Json
String shopJSon = JSONUtil.toJsonStr(shop);
//4.向redis存值
stringRedisTemplate.opsForValue().set(cacheShop+id,shopJSon);
return Result.ok(shop);
}
//商户类型缓存
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryList() {
//1.查询redis是否有值
String shopTypeJson = stringRedisTemplate.opsForValue().get(SHOP_TYPE_KEY);
//不为空返回数据
if (StrUtil.isNotBlank(shopTypeJson)){
//Json转list
List<ShopType> shopTypes = JSONUtil.parseArray(shopTypeJson).toList(ShopType.class);
return Result.ok(shopTypes);
}
//2.查询数据库判断是否有值
List<ShopType> typeList = query().orderByAsc("sort").list();
//为空返回错误
if (CollUtil.isEmpty(typeList)){
return Result.fail("商品分类为空");
}
//list转Json
String typeListJson = JSONUtil.toJsonStr(typeList);
//3.redis中存入数据
stringRedisTemplate.opsForValue().set(SHOP_TYPE_KEY,typeListJson);
return Result.ok(typeList);
}
}
2.缓存更新策略
@Override
public Result updateShopById(Shop shop) {
//获取店铺id并判断是否为空
Long id = shop.getId();
if (id == null){
return Result.fail("店铺不存在");
}
//更新店铺数据
updateById(shop);
//清除redis的数据
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
3.缓存穿透
@Override
public Result queryById(Long id) {
//1.从redis中查询
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.数据存在redis直接返回
if (StrUtil.isNotBlank(shopJson)){
//Json转bean
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//命中空值(防止缓存穿透)
if (Objects.equals(shopJson, "")){
return Result.fail("店铺不存在");
}
//3.不存在查询数据库
Shop shop = getById(id);
//判断数据库中是否存在
if(shop == null){
//缓存空值(防止缓存穿透)
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//Bean转Json
String shopJSon = JSONUtil.toJsonStr(shop);
//4.向redis存值
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,shopJSon,CACHE_SHOP_TTL,TimeUnit.MINUTES);
return Result.ok(shop);
}
4.缓存雪崩
5.缓存击穿
5.1互斥锁
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
//return queryWithPassThrough(id);
//1.从redis中查询
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.数据存在redis直接返回
if (StrUtil.isNotBlank(shopJson)){
//Json转bean
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//命中空值(防止缓存穿透)
if (Objects.equals(shopJson, "")){
return Result.fail("店铺不存在");
}
String lockKey = null;
Shop shop = null;
try {
//尝试上锁
lockKey = LOCK_SHOP_KEY + id;
Boolean flag = tryLock(lockKey);
if (!flag){
Thread.sleep(50);
queryById(id);
}
//3.不存在查询数据库
shop = getById(id);
//判断数据库中是否存在
if(shop == null){
//缓存控制(防止缓存穿透)
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//Bean转Json
String shopJSon = JSONUtil.toJsonStr(shop);
//4.向redis存值
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id,shopJSon,CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放锁
delLock(lockKey);
}
return Result.ok(shop);
}
private Boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.MINUTES);
return BooleanUtil.isTrue(flag);
}
private Boolean delLock(String key){
Boolean delete = stringRedisTemplate.delete(key);
return delete;
}
5.2 逻辑删除
@Resource
private StringRedisTemplate stringRedisTemplate;
//创建线程池
public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
@Override
public Result queryById(Long id) {
//1.从redis中查询
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.数据存在redis直接返回
if (StrUtil.isNotBlank(shopJson)){
//Json转bean
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//对比时间,判断是否过期
LocalDateTime expireTime= redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())){
return Result.ok(shop);
}
//上锁
Boolean flag = tryLock(LOCK_SHOP_KEY + id);
if (flag){
//提交一个线程
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
Shop newShop = getById(id);
saveShopRedis(id,newShop);
System.out.println("===========================调用新的线程");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
delLock(LOCK_SHOP_KEY + id);
System.out.println("===================释放锁");
}
});
}
return Result.ok(shop);
}
//命中空值(缓存穿透)
if (Objects.equals(shopJson, "")){
return Result.fail("店铺不存在");
}
//3.不存在查询数据库
Shop shop = getById(id);
//判断数据库中是否存在
if(shop == null){
//缓存控制(缓存穿透)
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
saveShopRedis(id,shop);
return Result.ok(shop);
}
private void saveShopRedis(Long id,Shop shop) {
//更新数据
RedisData newRedisData = new RedisData();
newRedisData.setData(shop);
newRedisData.setExpireTime(LocalDateTime.now().plusSeconds(20));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(newRedisData));
}
private Boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.MINUTES);
return BooleanUtil.isTrue(flag);
}
private Boolean delLock(String key){
Boolean delete = stringRedisTemplate.delete(key);
return delete;
}
工具封装
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
//解决缓存穿透
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;
}
//使用逻辑过期时间解决缓存击穿
public <R, ID> R queryWithLogicalExpire(
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.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
//使用互斥锁解决缓存击穿
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
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);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
调用
@Override
public Result queryById(Long id) {
Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);//(前缀,id,类型,查询数据库的方法,时间,单位)
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
五.优惠卷秒杀
1.全局唯一ID
package com.hmdp.utils;
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 {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
2.秒杀优惠卷
@Transactional
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher == null){
return Result.fail("优惠卷不存在");
}
//2.判断秒杀是否开始
LocalDateTime beginTime = voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())){
return Result.fail("活动尚未开始");
}
//3.判断秒杀是否结束 begin<now<end
LocalDateTime endTime = voucher.getEndTime();
if (endTime.isBefore(LocalDateTime.now())){
return Result.fail("活动已经结束");
}
//4.判断库存是否充足
Integer stock = voucher.getStock();
if (stock < 1){
return Result.fail("库存不足");
}
//5.扣减库存
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId,voucherId);
updateWrapper.set(SeckillVoucher::getStock,stock-1);
boolean flag = seckillVoucherService.update(null,updateWrapper);
if (!flag){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单
return Result.ok(orderId);
}
3.超卖问题
3.1 乐观锁
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId,voucherId);
//乐观锁()
updateWrapper.eq(SeckillVoucher::getStock,stock);
updateWrapper.set(SeckillVoucher::getStock,stock-1);
boolean flag = seckillVoucherService.update(null,updateWrapper);
以上方式解决了超卖问题但是成功率太低
解决
//5.扣减库存
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId,voucherId);
//乐观锁(改为gt)
updateWrapper.gt(SeckillVoucher::getStock,0); //乐观锁
updateWrapper.setSql("stock = stock-1");
boolean flag = seckillVoucherService.update(null,updateWrapper);
3.2.悲观锁
pom.xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
HmDianPingApplication添加@EnableAspectJAutoProxy
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
VoucherOrderServiceImpl.java
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher == null){
return Result.fail("优惠卷不存在");
}
//2.判断秒杀是否开始
LocalDateTime beginTime = voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())){
return Result.fail("活动尚未开始");
}
//3.判断秒杀是否结束 begin<now<end
LocalDateTime endTime = voucher.getEndTime();
if (endTime.isBefore(LocalDateTime.now())){
return Result.fail("活动已经结束");
}
//4.判断库存是否充足
Integer stock = voucher.getStock();
if (stock < 1){
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
//悲观锁
synchronized(userId.toString().intern()){//intern()从字符串池返回,防止不同对象带来的差异
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//采用AopContext方式,解决Spring事务注解失效
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单
Long userId = UserHolder.getUser().getId();
int count = lambdaQuery().eq(VoucherOrder::getVoucherId, voucherId).eq(VoucherOrder::getUserId, userId).count();
if (count>0){
return Result.fail("该优惠价不可重复抢购");
}
//5.扣减库存
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
updateWrapper.gt(SeckillVoucher::getStock,0); //乐观锁(优惠卷超卖)
//updateWrapper.set(SeckillVoucher::getStock,stock-1);
updateWrapper.setSql("stock = stock-1");
boolean flag = seckillVoucherService.update(null,updateWrapper);
if (!flag){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
voucherOrder.setUserId(userId);
//代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单
return Result.ok(orderId);
}
3.3并发安全问题(悲观锁失效)
如果有多台服务器,每台都有自己的锁对象,导致并发访问有多把锁
解决:使用分布式锁
六.分布式锁
1.解决并发安全问题
//分布式锁
//synchronized(userId.toString().intern())
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(60);
if (!isLock){
return Result.fail("该优惠价不可重复抢购");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
ILock.interface
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
SimpleRedisLock.java
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
2.分布式锁的原子性问题
线程1
判断标示一致后,jvm堵塞导致超时释放锁,线程2
开始执行。线程1
堵塞结束执行手动释放锁时,误删线程2
的锁,进而导致线程3
获取到锁.
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
unlock.lua
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
3.Redission
3.1配置
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
RedissonConfig.java
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
3.2入门使用
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
...省略代码
//分布式锁
// SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// boolean isLock = lock.tryLock(60);
RLock lock = redissonClient.getLock("lock:order:" + userId);//指定锁名称
boolean isLock = lock.tryLock();//不指定参数则不会重入
if (!isLock){
return Result.fail("该优惠价不可重复抢购");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
}
3.3 multiLock
4.异步
4.1.阻塞队列实现异步秒杀
seckill.lua
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
VoucherOrderServiceImpl.java
1.初始化lua脚本 —> 执行lua脚本
2.初始化阻塞队列 —> 添加订单信息到阻塞队列
3.创建线程池 —>在独立线程中获取队列信信息and创建订单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
//获取LUA脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
//初始化阻塞队列
private BlockingQueue<VoucherOrder> orderTasks= new ArrayBlockingQueue<VoucherOrder>(1024*1024);
//创建线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
//获取队列中的信息
VoucherOrder voucherOrder = orderTasks.take();
//创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("订单处理异常",e);
}
}
}
}
private IVoucherOrderService proxy;
private void handleVoucherOrder(VoucherOrder voucherOrder) {
/*如果此时调用方没有添加事务注解@Transactional,
而在被调用方添加事务注解@Transactional,
当被调用方法中出现异常,
这时候会发现事务并没有回滚,
事务注解@Transactional没有起作用*/
proxy.createVoucherOrder(voucherOrder);
}
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//1.执行LUA脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString());
int r = result.intValue();
if (r != 0){
return Result.fail(r==1?"库存不足":"该优惠价不可重复抢购");
}
//生成订单号
long orderId = redisIdWorker.nextId("order");
//添加订单到阻塞队,等待添加到数据库
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(userId);
orderTasks.add(voucherOrder);
//初始化代理
proxy = (IVoucherOrderService) AopContext.currentProxy();
//处理订单
return Result.ok(orderId);
}
@Transactional
@Override
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 扣减库存
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId, voucherOrder.getVoucherId());
updateWrapper.gt(SeckillVoucher::getStock,0); //乐观锁
//updateWrapper.set(SeckillVoucher::getStock,stock-1);
updateWrapper.setSql("stock = stock-1");
boolean flag = seckillVoucherService.update(null,updateWrapper);
if (!flag){
log.error("库存不足");
}
save(voucherOrder);
}
}
4.2.消息队列实现异步秒杀
5.redis实现消息队列
5.1.List模拟消息队列
5.2.pubsub
5.3.stream
5.4.stream — 消费者组
5.5.消息队列实现异步秒杀
创建队列和组
VoucherOrderServiceImpl.java
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
//获取LUA脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
//创建线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("订单处理异常",e);
handlePendingList();
}
}
}
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
private IVoucherOrderService proxy;
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//如果此时调用方没有添加事务注解@Transactional,而在被调用方添加事务注解@Transactional,当被调用方法中出现异常,这时候会发现事务并没有回滚,事务注解@Transactional没有起作用
proxy.createVoucherOrder(voucherOrder);
}
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//生成订单号
long orderId = redisIdWorker.nextId("order");
//1.执行LUA脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(),String.valueOf(orderId));
int r = result.intValue();
if (r != 0){
return Result.fail(r==1?"库存不足":"该优惠价不可重复抢购");
}
//初始化代理
proxy = (IVoucherOrderService) AopContext.currentProxy();
//处理订单
return Result.ok(orderId);
}
@Transactional
@Override
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 扣减库存
LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SeckillVoucher::getVoucherId, voucherOrder.getVoucherId());
updateWrapper.gt(SeckillVoucher::getStock,0); //乐观锁
//updateWrapper.set(SeckillVoucher::getStock,stock-1);
updateWrapper.setSql("stock = stock-1");
boolean flag = seckillVoucherService.update(null,updateWrapper);
if (!flag){
log.error("库存不足");
}
save(voucherOrder);
}
}
seckill.lua
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]
-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
七.
- 一
blogController.java
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@Resource
private IUserService userService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量
// blogService.update()
// .setSql("liked = liked + 1").eq("id", id).update();
return blogService.likeBlog(id);
}
@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
@GetMapping("/of/user")
public Result getUserBlog(@RequestParam("id") Long id,@RequestParam("current") Long current ){
return blogService.getUserBlog(id,current);
}
@GetMapping("of/follow")
public Result getFollowBlog(@RequestParam("lastId") Long lastID ,@RequestParam(value = "offset",defaultValue = "0") Integer offset){
return blogService.getFollowBlog(lastID,offset);
}
@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
return blogService.queryBlogById(id);
}
@GetMapping("/likes/{id}")
public Result queryBlogLikesById(@PathVariable("id") Long id){
return blogService.queryBlogLikesById(id);
}
}
bolgServiceImpl.java
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IFollowService followService;
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog -> {
this.blogSetUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
@Override
public Result queryBlogLikesById(Long id) {
//查询钱五个点赞的
String blog_Liked_Key = BLOG_LIKED_KEY + id;
Set<String> range = stringRedisTemplate.opsForZSet().range(blog_Liked_Key, 0, 4);
//判断是否有人点赞
if (range == null || range.isEmpty()){
return Result.ok(Collections.emptyList());
}
//Set<String>转 List<Long>
List<Long> top5 = range.stream().map(s -> Long.valueOf(s)).collect(Collectors.toList());
//通过id查询用户并转换成UserDTO
List<UserDTO> userDTOS = userService.listByIds(top5).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(userDTOS);
}
@Override
public Result getUserBlog(Long id, Long current) {
Page<Blog> blogPage = this.lambdaQuery().eq(Blog::getUserId, id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
List<Blog> blogs = blogPage.getRecords();
if (blogs == null || blogs.isEmpty() ){
return Result.ok(Collections.emptyList());
}
return Result.ok(blogs);
}
@Override
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
if (blog.getTitle() == null ){
return Result.fail("笔记标题不能为空");
}
// 保存探店博文
boolean isSave = save(blog);
if (!isSave){
return Result.fail("新增笔记失败");
}
//获取所以关注
List<Follow> follows = followService.lambdaQuery().eq(Follow::getFollowUserId, user.getId()).list();
for (Follow follow : follows) {
Long userId = follow.getUserId();
stringRedisTemplate.opsForZSet().add(FEED_KEY+userId,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
@Override
public Result getFollowBlog(Long lastID, Integer offset) {
Long userId = UserHolder.getUser().getId();
//查询收件箱
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(FEED_KEY + userId, 0, lastID, offset, 2);
if (typedTuples == null || typedTuples.isEmpty() ){
return Result.ok();
}
//解析查询到的数据
List<Long> ids = new ArrayList<>(typedTuples.size());
int os = 1;
long minTime = 0;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
ids.add(Long.valueOf(typedTuple.getValue()));
long time = typedTuple.getScore().longValue();
if (time == minTime){
os++;
}else {
minTime = time;
os = 1;
}
}
//根据id获取blog
List<Blog> blogs = lambdaQuery().in(Blog::getId, ids).orderBy(true, false, Blog::getCreateTime).list();
for (Blog blog : blogs) {
//查询点赞用户
queryBlogById(blog.getId());
//查询是否点赞
isBlogLiked(blog);
}
ScrollResult result = new ScrollResult();
result.setList(blogs);
result.setOffset(os);
result.setMinTime(minTime);
return Result.ok(result);
}
private void isBlogLiked(Blog blog) {
UserDTO user = UserHolder.getUser();
if (user == null){
return;
}
Long userId = user.getId();
String blog_Liked_Key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(blog_Liked_Key, userId.toString());
blog.setIsLike(score != null);
}
@Override
public Result queryBlogById(Long id) {
Blog blog = getById(id);
if (blog == null) {
return Result.fail("博客不存在");
}
//设置blog的用户信息
this.blogSetUser(blog);
//是否点赞
this.isBlogLiked(blog);
return Result.ok(blog);
}
private Long blogSetUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setIcon(user.getIcon());
blog.setName(user.getNickName());
return userId;
}
@Override
public Result likeBlog(Long id) {
String blog_Liked_Key = BLOG_LIKED_KEY + id;
String userId = UserHolder.getUser().getId().toString();
//1.判断用户是否点过赞
Double score = stringRedisTemplate.opsForZSet().score(blog_Liked_Key, userId);
if (score == null) {
//没点过赞
boolean isSuccess = lambdaUpdate().eq(Blog::getId, id).setSql("liked = liked + 1").update();
if (isSuccess) {
stringRedisTemplate.opsForZSet().add(blog_Liked_Key, userId,System.currentTimeMillis());
}
} else {
//点过赞
boolean isSuccess = lambdaUpdate().eq(Blog::getId, id).setSql("liked = liked - 1").update();
if (isSuccess) {
stringRedisTemplate.opsForZSet().remove(blog_Liked_Key, userId);
}
}
return Result.ok();
}
}
- 二.
followController.java
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId,@PathVariable("isFollow") Boolean isFollow){
return followService.follow(followUserId,isFollow);
}
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId){
return followService.isFollow(followUserId);
}
@GetMapping("/common/{id}")
public Result commonConcern(@PathVariable Long id){
return followService.commonConcern(id);
}
}
followServiceImpl.java
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
//关注和取关
@Override
public Result follow(Long followUserid, Boolean isFollow) {
Long userId = UserHolder.getUser().getId();
String followKey = "follows"+userId;
if (isFollow) {
//初始化follow
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserid);
follow.setCreateTime(LocalDateTime.now());
//关注
boolean isSave = this.save(follow);
if (isSave){
stringRedisTemplate.opsForSet().add(followKey,followUserid.toString());
}
return Result.ok();
}
//取关
boolean isRemove = this.lambdaUpdate().eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserid).remove();
if (isRemove) {
stringRedisTemplate.opsForSet().remove(followKey,followUserid.toString());
}
return Result.ok();
}
//查询是否关注
@Override
public Result isFollow(Long followUserId) {
Long userId = UserHolder.getUser().getId();
//查询是否关注
Integer count = this.lambdaQuery().eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId).count();
return Result.ok(count > 0);
}
@Override
public Result commonConcern(Long id) {
Long userId = UserHolder.getUser().getId();
String myKey = "follows"+userId;
String otherKey = "follows"+id;
Set<String> resultSet = stringRedisTemplate.opsForSet().intersect(myKey, otherKey);
if (resultSet == null || resultSet.isEmpty()){
return Result.ok();
}
//Set<String>转List<Long>
List<Long> resultList = resultSet.stream().map(s -> Long.valueOf(s)).collect(Collectors.toList());
List<UserDTO> users = userService.listByIds(resultList).stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
if (users == null || users.isEmpty()){
return Result.ok(Collections.emptyList());
}
return Result.ok(users);
}
}
geo类型
bitMap
hyperLogLog
八.持久化