🌈hello,你好鸭,我是Ethan,一名不断学习的码农,很高兴你能来阅读。
✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。
🏃人生之义,在于追求,不在成败,勤通大道。加油呀!
🔥个人主页:Ethan Yankang
🔥专栏:史上最强八股文||Java项目
🔥温馨提示:划到文末发现专栏彩蛋 点击这里直接传送
🔥本篇概览:详细讲解了黑马点评项目的源码,以先围绕业务模块进行讲解为引,再针对项目源码结构进行完善,既节省时间,又一目了然。🌈⭕🔥
【计算机领域一切迷惑的源头都是基本概念的模糊,算法除外】
1.项目总览
1.1核心架构8大件
业务流程全打通
entity——>controller——>service——>serviceImpl——>Mapper
2.完整源码实现讲解
hmdp
config
MvcConfig
@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刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
MybatisConfig
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
RedissonConfig
@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);
}
}
WebExceptionAdvice
@Slf4j
@RestControllerAdvice
public class WebExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public Result handleRuntimeException(RuntimeException e) {
log.error(e.toString(), e);
return Result.fail("服务器异常");
}
}
controller
BlogCommentsController
@RestController
@RequestMapping("/blog-comments")
public class BlogCommentsController {
}
BlogController
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
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("/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 queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}
@GetMapping("/of/user")
public Result queryBlogByUserId(
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam("id") Long id) {
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max, offset);
}
}
FollowController
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private 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 followCommons(@PathVariable("id") Long id){
return followService.followCommons(id);
}
}
ShopController
@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);
}
/**
* 新增商铺信息
* @param shop 商铺数据
* @return 商铺id
*/
@PostMapping
public Result saveShop(@RequestBody Shop shop) {
// 写入数据库
shopService.save(shop);
// 返回店铺id
return Result.ok(shop.getId());
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}
/**
* 根据商铺类型分页查询商铺信息
* @param typeId 商铺类型
* @param current 页码
* @return 商铺列表
*/
@GetMapping("/of/type")
public Result queryShopByType(
@RequestParam("typeId") Integer typeId,
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam(value = "x", required = false) Double x,
@RequestParam(value = "y", required = false) Double y
) {
return shopService.queryShopByType(typeId, current, x, y);
}
/**
* 根据商铺名称关键字分页查询商铺信息
* @param name 商铺名称关键字
* @param current 页码
* @return 商铺列表
*/
@GetMapping("/of/name")
public Result queryShopByName(
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "current", defaultValue = "1") Integer current
) {
// 根据类型分页查询
Page<Shop> page = shopService.query()
.like(StrUtil.isNotBlank(name), "name", name)
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 返回数据
return Result.ok(page.getRecords());
}
}
@GetMapping("/{id}") public Result queryShopById(@PathVariable("id") Long id) { return shopService.queryById(id); }
1、public Result queryShopById(@PathVariable("id") Long id)
1.1、@PathVariable("id") Long id,这里获取了路径中的参数存在id之中,作为查询对象。
1.2、而@RequestParam("phone") String phone 一般是前台传过来的表格信息里面的数据,进行查询。
1.3、return shopservice.queryById(id);调用接口方法,体现分层思想。
ShopTypeController
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
List<ShopType> typeList = typeService
.query().orderByAsc("sort").list();
return Result.ok(typeList);
}
}
Uploadcontroller
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {
@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
@GetMapping("/blog/delete")
public Result deleteBlogImg(@RequestParam("name") String filename) {
File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename);
if (file.isDirectory()) {
return Result.fail("错误的文件名称");
}
FileUtil.del(file);
return Result.ok();
}
private String createNewFileName(String originalFilename) {
// 获取后缀
String suffix = StrUtil.subAfter(originalFilename, ".", true);
// 生成目录
String name = UUID.randomUUID().toString();
int hash = name.hashCode();
int d1 = hash & 0xF;
int d2 = (hash >> 4) & 0xF;
// 判断目录是否存在
File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));
if (!dir.exists()) {
dir.mkdirs();
}
// 生成文件名
return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
}
}
UserController
@Slf4j
@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 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
/**
* 登出功能
* @return 无
*/
@PostMapping("/logout")
public Result logout(){
// TODO 实现登出功能
return Result.fail("功能未完成");
}
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long userId){
// 查询详情
UserInfo info = userInfoService.getById(userId);
if (info == null) {
// 没有详情,应该是第一次查看详情
return Result.ok();
}
info.setCreateTime(null);
info.setUpdateTime(null);
// 返回
return Result.ok(info);
}
@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){
// 查询详情
User user = userService.getById(userId);
if (user == null) {
return Result.ok();
}
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 返回
return Result.ok(userDTO);
}
@PostMapping("/sign")
public Result sign(){
return userService.sign();
}
@GetMapping("/sign/count")
public Result signCount(){
return userService.signCount();
}
}
1.sendCode(@RequestParam("phone") String phone, HttpSession session)
1.1@RequestParam("phone") String phone 参数绑定
@RequestParam("phone"):这是一个 Spring 框架中的注解。它用于将请求参数(在这里是名为"phone"的参数)绑定到方法的参数上。
"phone":指定了要绑定的请求参数的名称。
String phone:这是方法接收绑定后的参数的变量,类型为String,表示接收到的请求中名为phone的参数的值会被存储到这个变量中。
HttpSession httpSession:这是一个代表当前 HTTP 会话的对象。通过它可以进行与当前会话相关的操作,比如存储或获取会话中的数据等。
1.2分层思想
这里的Controller层只调用service层的接口方法,接口的实现再具体实现方法
2.login(@RequestBody LoginFormDTO loginForm, HttpSession session)
2.1注意前端提交的是json风格,所以到后端一定要在参数前加上@requireBody的注解。
VoucherController
@RestController
@RequestMapping("/voucher")
public class VoucherController {
@Resource
private IVoucherService voucherService;
/**
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
/**
* 新增普通券
* @param voucher 优惠券信息
* @return 优惠券id
*/
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {
voucherService.save(voucher);
return Result.ok(voucher.getId());
}
/**
* 查询店铺的优惠券列表
* @param shopId 店铺id
* @return 优惠券列表
*/
@GetMapping("/list/{shopId}")
public Result queryVoucherOfShop(@PathVariable("shopId") Long shopId) {
return voucherService.queryVoucherOfShop(shopId);
}
}
VoucherOrderController
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
dto
LoginFormDTO
前台打过来的数据
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
1.注意这里是数据传输层DTO(data transfer Object),专门用来从前端传输数据到后端。
2.这里是既支持密码登录,也支持直接手机验证码登录,所以有一个password
ScrollResult
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
UserDTO
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
entity
User
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements java.io.Serializable{
private static final long serialVersionUID = 1L;
// 主键
@TableId(value = "id",type= IdType.AUTO)
private Long id;
// 手机号码
private String phone;
// 密码,加密存储
private String password;
//昵称,默认是随机字符
private String nickName;
// 用户头像
private String icon="";
// 创建时间
private LocalDateTime createTime;
// 更新时间
private LocalDateTime updateTime;
}
1.注解
@EqualsAndHashCode(callSuper = false):这是 Lombok 提供的注解,用于生成equals和hashCode方法。callSuper = false表示在生成equals和hashCode方法时不考虑父类的属性。
@Accessors(chain = true):这也是 Lombok 提供的注解,用于生成链式访问器方法。chain = true表示生成的setter方法返回当前对象,以便进行链式调用。
@TableName("tb_user"):这是 MyBatis-Plus 提供的注解,用于将实体类与数据库表进行映射。"tb_user"是数据库表的名称。
2.序列化实现
//Serializable 是 Java 中的一个标记接口,它没有任何方法。
// 实现该接口的类可以被序列化,即将对象的状态转换为字节流,以便在网络传输或存储到文件中。
//通过实现 Serializable 接口,User 类的对象可以使用 Java 的序列化机制进行序列化和反序列化操作。这使得对象可以在不同的 Java 程序或系统之间进行传输和存储,并能够保持对象的状态和数据完整性。
3.mp与数据库交互的主键
// @TableId(value = "id", type = IdType.AUTO) 是 MyBatis-Plus 框架中的一个注解,用于定义实体类中主键的属性。其中,value 属性指定了主键字段的名称,type 属性指定了主键的生成策略。
// 在上述代码中,type = IdType.AUTO 表示主键将采用数据库自增的方式生成。当插入一条新记录时,数据库会自动为 id 字段分配一个唯一的值。
// 如果没有添加 @TableId 注解,MyBatis-Plus 将无法确定哪个字段是主键,可能会导致数据操作异常。
4.Entity与数据库
项目中的每一个表结构都会对应一个实体类entity,也有可能有Mapper层(简单的就直接用MP不用Mapper层)。
mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
iservice
IUserService
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession httpsession);
}
1.注意接口只能有继承,没有实现。
2.注意服务类接口继承的是Iservice<entity>。
3.注意这里的Result类,作为数据返回的统一封装。
IShopService
public interface IShopService extends IService<Shop> {
Result queryById(Long id);
Result update(Shop shop);
Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}
有关Shop的业务操作接口定义全在这里了。
iserviceImpl
UserServiceImpl
目前是直接针对redis的的验证,先将业务流程理清楚(如下面的1.2.3.4.5点),再逐步实现。
@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.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到 session
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 = ?
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);
}
@Override
public Result sign() {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.写入Redis SETBIT key offset 1
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
}
@Override
public Result signCount() {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
);
if (result == null || result.isEmpty()) {
// 没有任何签到结果
return Result.ok(0);
}
Long num = result.get(0);
if (num == null || num == 0) {
return Result.ok(0);
}
// 6.循环遍历
int count = 0;
while (true) {
// 6.1.让这个数字与1做与运算,得到数字的最后一个bit位 // 判断这个bit位是否为0
if ((num & 1) == 0) {
// 如果为0,说明未签到,结束
break;
}else {
// 如果不为0,说明已签到,计数器+1
count++;
}
// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位
num >>>= 1;
}
return Result.ok(count);
}
private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}
}
1.public Result sendCode(String phone, HttpSession session)
注意,随机字符串工具hutools
String code = RandomUtil.randomNumbers(6);
2.保存验证码到 redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
·这段代码是使用 Spring Data Redis 中的 StringRedisTemplate 来执行一些操作。
以下是对代码的详细解释:
stringRedisTemplate.opsForValue():获取用于操作字符串值的相关操作对象。
.set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES):这是执行设置值的操作。
ShopServiceImpl
@Service
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);
}
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// 1.判断是否需要根据坐标查询
if (x == null || y == null) {
// 不需要坐标查询,按数据库查询
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
return Result.ok(page.getRecords());
}
// 2.计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
// 3.查询redis、按照距离排序、分页。结果:shopId、distance
String key = SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 4.解析出id
if (results == null) {
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
if (list.size() <= from) {
// 没有下一页了,结束
return Result.ok(Collections.emptyList());
}
// 4.1.截取 from ~ end的部分
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result -> {
// 4.2.获取店铺id
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
// 4.3.获取距离
Distance distance = result.getDistance();
distanceMap.put(shopIdStr, distance);
});
// 5.根据id查询Shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
// 6.返回
return Result.ok(shops);
}
}
1.public Result queryById(Long id)
1.1这个方法将要解决缓存穿透(cacheClient.queryWithPassThrough)、缓存击穿((cacheClient.queryWithMutex)或(cacheClient .queryWithLogicalExpire))的问题,具体分析点击这里:黑马点评2——商户查询缓存篇-CSDN博客
这里的解决方案是在utils包中的CacheClient类中编写逻辑。
2.public Result update(Shop shop)
注意这里是要加事务注解的(要么全部执行,要么全执行不成功)
@Transactional
3.public Result queryShopByType(Integer typeId, Integer current, Double x, Double y)
注意这里是根据距离查询商户信息,并且分页显示。
utils
CacheClient
@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);
}
}
这个类是用来判断缓存穿透和缓存击穿的。
1、缓存穿透:
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit)
这里的逻辑见详见此
Result
@Data
@NoArgsConstructor
@AllArgsConstructor
//很好的一个封装类
public class Result {
private Boolean success;
private String erroeMsg;
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);
}
// List<?> data是可以接受一个泛型参数列表的
public static Result ok(List<?> data, Long total){
return new Result(true,null,data,total);
}
public static Result fail(){
return new Result(false,"fail",null,null);
}
}
1.注意注解的使用
@Data
:该注解通常用在实体类上,它相当于同时使用了@Getter
、@Setter
、@RequiredArgsConstructor
、@ToString
和@EqualsAndHashCode
这 5 个注解。使用@Data
注解可以为类提供读写属性的方法,以及equals()
、hashCode()
和toString()
方法。@NoArgsConstructor
:该注解用于生成一个无参的构造函数。如果类中没有定义任何构造函数,那么 Lombok 会自动生成一个默认的无参构造函数。@AllArgsConstructor
:该注解用于生成一个包含所有参数的构造函数。使用该注解可以减少代码量,避免手动编写多个构造函数。
2.注意java 基础的掌握,对象的概念(属性+操作)。
3.可以记住这种很好的封装方法,以后方便使用。
4.通过以下方式自动引出实现层
RegexUtils
验证输入的手机号、邮箱号是否符合格式。
package com.hmdp.utils;
import cn.hutool.core.util.StrUtil;
/**
* @author 虎哥
*/
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
/**
* 是否是无效邮箱格式
* @param email 要校验的邮箱
* @return true:符合,false:不符合
*/
public static boolean isEmailInvalid(String email){
return mismatch(email, RegexPatterns.EMAIL_REGEX);
}
/**
* 是否是无效验证码格式
* @param code 要校验的验证码
* @return true:符合,false:不符合
*/
public static boolean isCodeInvalid(String code){
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
}
RegexPatterns
手机号、邮箱号、密码、验证码的正则表达式。
package com.hmdp.utils;
/**
* @author 虎哥
*/
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}$";
}
RedisConstants
package com.hmdp.utils;
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
public static final Long CACHE_NULL_TTL = 2L;
public static final Long CACHE_SHOP_TTL = 30L;
public static final String CACHE_SHOP_KEY = "cache:shop:";
public static final String LOCK_SHOP_KEY = "lock:shop:";
public static final Long LOCK_SHOP_TTL = 10L;
public static final String SECKILL_STOCK_KEY = "seckill:stock:";
public static final String BLOG_LIKED_KEY = "blog:liked:";
public static final String FEED_KEY = "feed:";
public static final String SHOP_GEO_KEY = "shop:geo:";
public static final String USER_SIGN_KEY = "sign:";
}
常量类,static final修饰变量使得是类级别且不可更改。
HMDianPingApplication
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
1.注意这里的注解MapperScan("com.mapper")是指扫描那个包下的类作为Mapper层
resources
db
mapper
UserMapper
public interface UserMapper extends BaseMapper<User> {
}
1.注意要有@Mapper注解,或者直接在Springboot启动类之上添加扫描注解
@MapperScan("com.hmdp.mapper"),这里采用后者。
不然不会加入到spring中。
2.注意要继承BaseMapper<User>。
有关spring的详细介绍,请点击这里
application.yml
seckill.lua
unlock.lua
test
hmdp
HmDianPingApplicationTests
NormalTest
RedissonTest
💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖
热门专栏推荐
🌈🌈算机科学入门系列 关注走一波💕💕
🌈🌈CSAPP深入理解计算机原理 关注走一波💕💕
🌈🌈微服务项目之黑马头条 关注走一波💕💕
🌈🌈redis深度项目之黑马点评 关注走一波💕💕
🌈🌈Java面试八股文系列专栏 关注走一波💕💕
🌈🌈算法leetcode+剑指offer 关注走一波💕💕
最全专栏
🌈🌈spring 关注走一波💕💕
🌈🌈redis 关注走一波💕💕
🌈🌈MySQL 关注走一波💕💕
🌈🌈mybatis 关注走一波💕💕
🌈🌈MQ 关注走一波💕💕
🌈🌈微服务 关注走一波💕💕
🌈🌈设计模式 关注走一波💕💕
🌈🌈分布式锁 关注走一波💕💕
🌈🌈JAVA面试八股文(redis、MySQL、框架、微服务、消息中间件、JVM、设计模式、并发编程、JAVA集合、常见技术场景)
关注走一波💕💕
🌈🌈黑马头条(微服务) 关注走一波💕💕
🌈🌈黑马点评(redis) 关注走一波💕💕
🌈🌈计算机四件套
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机网络 关注走一波💕💕
🌈🌈数据结构与算法 关注走一波💕💕
🌈🌈算法
🌈🌈leetcode 关注走一波💕💕
🌈🌈剑指offer 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总
🌈🌈CSAPP笔记 关注走一波💕💕
🌈🌈计算机科学速成课 关注走一波💕💕
🌈🌈CS自学指南 关注走一波💕💕
🌈🌈读书笔记与每日记录 关注走一波💕💕
🌈🌈C/C++技术栈
关注走一波💕💕
🌈🌈GO技术栈
关注走一波💕💕
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐!
💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖
热门专栏推荐
🌈🌈计算机科学入门系列 关注走一波💕💕
🌈🌈CSAPP深入理解计算机原理 关注走一波💕💕
🌈🌈微服务项目之黑马头条 关注走一波💕💕
🌈🌈redis深度项目之黑马点评 关注走一波💕💕
🌈🌈Java面试八股文系列专栏 关注走一波💕💕
🌈🌈算法leetcode+剑指offer 关注走一波💕💕
总栏
🌈🌈JAVA后端技术栈 关注走一波💕💕
🌈🌈JAVA面试八股文 关注走一波💕💕
🌈🌈JAVA项目(含源码深度剖析) 关注走一波💕💕
🌈🌈计算机四件套 关注走一波💕💕
🌈🌈算法 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
分栏
🌈🌈JAVA后端技术栈
🌈🌈spring 关注走一波💕💕
🌈🌈redis 关注走一波💕💕
🌈🌈MySQL 关注走一波💕💕
🌈🌈mybatis 关注走一波💕💕
🌈🌈MQ 关注走一波💕💕
🌈🌈微服务 关注走一波💕💕
🌈🌈设计模式 关注走一波💕💕
🌈🌈分布式锁 关注走一波💕💕
🌈🌈JAVA八股文JAVA面试八股文(redis、MySQL、框架、微服务、MQ、JVM、设计模式、并发编程、JAVA集合、常见技术场景) 关注走一波💕💕
🌈🌈JAVA项目(含源码深度剖析)
🌈🌈黑马头条(微服务) 关注走一波💕💕
🌈🌈黑马点评(redis) 关注走一波💕💕
🌈🌈计算机四件套
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机网络 关注走一波💕💕
🌈🌈数据结构与算法 关注走一波💕💕
🌈🌈算法
🌈🌈leetcode 关注走一波💕💕
🌈🌈剑指offer 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总
🌈🌈CSAPP笔记 关注走一波💕💕
🌈🌈计算机科学速成课 关注走一波💕💕
🌈🌈CS自学指南 关注走一波💕💕
🌈🌈读书笔记与每日记录 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐!
💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖
热门专栏推荐
🌈🌈计算机科学入门系列 关注走一波💕💕
🌈🌈CSAPP深入理解计算机原理 关注走一波💕💕
🌈🌈微服务项目之黑马头条 关注走一波💕💕
🌈🌈redis深度项目之黑马点评 关注走一波💕💕
🌈🌈Java面试八股文系列专栏 关注走一波💕💕
🌈🌈算法leetcode+剑指offer 关注走一波💕💕
总栏
🌈🌈JAVA后端技术栈 关注走一波💕💕
🌈🌈JAVA面试八股文 关注走一波💕💕
🌈🌈JAVA项目(含源码深度剖析) 关注走一波💕💕
🌈🌈计算机四件套 关注走一波💕💕
🌈🌈算法 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
分栏
🌈🌈JAVA后端技术栈
🌈🌈spring 关注走一波💕💕
🌈🌈redis 关注走一波💕💕
🌈🌈MySQL 关注走一波💕💕
🌈🌈mybatis 关注走一波💕💕
🌈🌈mybatisplus 关注走一波💕💕
🌈🌈MQ 关注走一波💕💕
🌈🌈微服务 关注走一波💕💕
🌈🌈设计模式 关注走一波💕💕
🌈🌈分布式锁 关注走一波💕💕
🌈🌈JAVA八股文
JAVA面试八股文(redis、MySQL、框架、微服务、MQ、JVM、设计模式、并发编程、JAVA集合、常见技术场景)
🌈🌈JAVA项目(含源码深度剖析)
🌈🌈黑马头条(微服务) 关注走一波💕💕
🌈🌈黑马点评(redis) 关注走一波💕💕
🌈🌈计算机四件套
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机基础 关注走一波💕💕
🌈🌈计算机网络 关注走一波💕💕
🌈🌈数据结构与算法 关注走一波💕💕
🌈🌈算法
🌈🌈leetcode 关注走一波💕💕
🌈🌈剑指offer 关注走一波💕💕
🌈🌈必知必会工具集 关注走一波💕💕
🌈🌈书籍网课笔记汇总
🌈🌈CSAPP笔记 关注走一波💕💕
🌈🌈计算机科学速成课 关注走一波💕💕
🌈🌈CS自学指南 关注走一波💕💕
🌈🌈读书笔记与每日记录 关注走一波💕💕
🌈🌈考试复习资料 关注走一波💕💕
🌈🌈C/C++技术栈 关注走一波💕💕
🌈🌈GO技术栈 关注走一波💕💕
📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐