1.实现基于session的登录功能
在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);
}
@PostMapping("/sign")
public Result sign(){
return userService.sign();
}
@GetMapping("/sign/count")
public Result signCount(){
return userService.signCount();
}
}
在IUserService层实现方法上述四个方法的接口层
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
Result login(LoginFormDTO loginForm, HttpSession session);
Result sign();
Result signCount();
}
在 UserServiceImpl改写上面这四个方法,并且实现具体的逻辑
extends ServiceImpl<UserMapper, User>是为了改写接口层中的方法
implements IUserService是为了继承IUserService操作数据库的方法
@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);
}
2.基于redis缓存的商品查询功能,利用互斥锁解决缓存击穿问题,采取缓存空对象应对缓存穿透,缓存更新策略
@RestController
@RequestMapping("/shop")
public class ShopController {
@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);
}
public interface IShopService extends IService<Shop> {
Result queryById(Long id);
Result update(Shop shop);
Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
Shop shop =queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
//下面是利用互斥锁解决缓存击穿的代码
public Shop queryWithMutex(Long id){
String key = CACHE_SHOP_KEY +id;
//1.从redis中查询缓存,redis中查到的都是json形式
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否redis中存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在则直接返回,但此时shopjson的信息还是json形式,要转成java形式(缓存形式)
//3.(缓存穿透形式)存在,先判断是否为空值,是则直接结束,否返回商铺信息
return JSONUtil.toBean(shopJson, Shop.class);
}
//命中的是否是空值,shopJson!= null,shopJson不为空就代表是空值“”
if (shopJson!= null) {
return null;
}
Shop shop = null;
//4.利用互斥锁解决缓存击穿(热点key问题)
//4.1获取互斥锁
String lockKey = LOCK_SHOP_KEY +id;
try {
boolean isLock = tryGetLock(lockKey);
//4.2判断是否获取锁成功
if (!isLock) {
//4.3失败, 休眠并且重试
Thread.sleep(50);
return queryWithMutex(id);//重试就是递归调用该方法,直至有人释放锁或者再缓存中拿到数据
}
//4.4获取锁成功 则到数据库中查对应id
shop = getById(id);
//5.id不存在则报错
if (shop==null) {
//(缓存穿透)存入redis中空值
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//(缓存版)返回错误信息
return null;
}
//6.存在则写入redis并返回,采取超时剔除策略与主动更新策略辅助,超时剔除就是为写入redis操作设置一个超时时间
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop) ,CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁
unLock(lockKey);
}
//8.返回数据
return shop;
}
//尝试获取互斥锁,利用redis的setnx当互斥锁
private Boolean tryGetLock(String lock){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lock, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放互斥锁
private void unLock(String lock){
stringRedisTemplate.delete(lock);
}
@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();
}
3. 优惠券秒杀功能