赞
踩
项目介绍:
实现发送短信:
此处没有实现真正的短信发布功能,需结合阿里云短信服务。
package com.hmdp.service.impl; import cn.hutool.core.util.RandomUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.dto.Result; import com.hmdp.entity.User; import com.hmdp.mapper.UserMapper; import com.hmdp.service.IUserService; import com.hmdp.utils.RegexUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.servlet.http.HttpSession; /** * <p> * 服务实现类 * </p> * */ @Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @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 session.setAttribute("code",code); //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)){ return Result.fail("手机号格式错误!"); } //2. 校验验证码 Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); if(cacheCode == null || !cacheCode.toString().equals(code)){ //3. 不一致,报错 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. 保存用户信息到session中 session.setAttribute("user",user); return Result.ok(); } 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; }
LoginInterceptor
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(); } }
加入MVC文件:
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/**" ); } }
session 共享问题:多台Tomcat并不共享session 存储空间,当请求切换到不同Tomcat服务时导致数据丢失的问题。
基于Redis实现共享session登录:
Service方法中:
修改拦截器:
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 javax.servlet.http.HttpSession; import java.util.Map; import java.util.concurrent.TimeUnit; public class LoginInterceptor implements HandlerInterceptor { //这是手动创建的类,不能自动注入 转到MvcConfig private StringRedisTemplate stringRedisTemplate; public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的Token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { // 不存在,拦截 response.setStatus(401); return false; } // 2.基于Token获取Redis中的用户 Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token); // 3.判断用户是否存在 if(userMap.isEmpty()){ //4.不存在,拦截 response.setStatus(401); return false; } // 5. 将查询到的Hash数据转为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // TODO 7. 刷新Token有效期 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.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.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 RefreshTokenInterceptor implements HandlerInterceptor { //这是手动创建的类,不能自动注入 转到MvcConfig private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的Token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2.基于Token获取Redis中的用户 Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token); // 3.判断用户是否存在 if(userMap.isEmpty()){ return true; /*//4.不存在,拦截 response.setStatus(401); return false;*/ } // 5. 将查询到的Hash数据转为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // 7. 刷新Token有效期 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.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.utils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 判断是否需要拦截(ThreadLocal中是否有用户) if(UserHolder.getUser() == null){ // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } }
需修改:MvcConfig
此功能是先用session实现了一遍,然后通过redis来实现session,最终基于redis来成功实现短信登录功能。
@Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { // 1.从redis查询商品缓存 String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); // 2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { // 3.存在,直接返回 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } // 4.不存在,根据id查询数据库 Shop shop = getById(id); // 5.不存在,返回错误 if(shop == null){ return Result.fail("店铺不存在!"); } // 6.存在,写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); // 7.返回 return Result.ok(shop); }
ShopServiceImpl:
@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();
}
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决方法:
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方法:
package com.hmdp.service.impl; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; 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 org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; /** * <p> * 服务实现类 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { // 缓存穿透: // Shop shop = queryWithPassThrough(id); // 用互斥锁解决缓存击穿 Shop shop = queryWithMutex(id); if(shop == null){ return Result.fail("店铺不存在!"); } // 7.返回 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 void unlock(String key){ stringRedisTemplate.delete(key); } // 封装方法 【缓存击穿】 public Shop queryWithMutex(Long id){ // 1.从redis查询商品缓存 String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); // 2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { // 3.存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } // 【缓存穿透】判断命中的是否为空值 if(shopJson != null){ // 返回一个错误信息 return null; } // 4. 实现缓存重建 // 4.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; Shop shop = null; try { boolean isLock = tryLock(lockKey); // 4.2 判断是否获取成功 if (!isLock){ // 4.3 失败,则休眠并重试 Thread.sleep(50); return queryWithMutex(id); } // 4.4 成功, // 4.4.1 (获取锁成功应该再次检测redis缓存是否存在,做DoubleCheck。如果存在,则无需重建缓存。) // 4.4.2 根据id查询数据库 shop = getById(id); // 模拟重建的延时 Thread.sleep(200); // 5.不存在,返回错误 if(shop == null){ // 【缓存穿透】将空值写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回错误信息 return null; } // 6.存在,写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(); } finally { // 7. 释放互斥锁 unlock(lockKey); } // 8. 返回 return shop; } // 封装方法 【缓存穿透】 public Shop queryWithPassThrough(Long id){ // 1.从redis查询商品缓存 String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); // 2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { // 3.存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } // 【缓存穿透】判断命中的是否为空值 if(shopJson != null){ // 返回一个错误信息 return null; } // 4.不存在,根据id查询数据库 Shop shop = getById(id); // 5.不存在,返回错误 if(shop == null){ // 【缓存穿透】将空值写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回错误信息 return null; } // 6.存在,写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); // 7.返回 return 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(); } }
封装互斥锁方法和封装缓存击穿和缓存穿透来实现其他功能调用。
package com.hmdp.utils; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; 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.TimeUnit; import java.util.function.Function; import static com.hmdp.utils.RedisConstants.*; @Slf4j @Component public class CacheClient { private final StringRedisTemplate stringRedisTemplate; public CacheClient(StringRedisTemplate stringRedisTemplate){ this.stringRedisTemplate = stringRedisTemplate; } private void set(String key, Object value, Long time, TimeUnit unit){ stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit); } private 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)); } // Redis 工具类 【缓存穿透】 public <T,ID> T queryWithPassThrough(String keyPrefix, ID id, Class<T> type, Function<ID, T> dbFallback, Long time, TimeUnit unit){ // 1.从redis查询商品缓存 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isNotBlank(json)) { // 3.存在,直接返回 return JSONUtil.toBean(json, type); } // 【缓存穿透】判断命中的是否为空值 if(json != null){ // 返回一个错误信息 return null; } // 4.不存在,根据id查询数据库 T t = dbFallback.apply(id); // 5.不存在,返回错误 if(t == null){ // 【缓存穿透】将空值写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回错误信息 return null; } // 6.存在,写入redis this.set(key, t, time, unit); // 7.返回 return t; } // Redis 工具类 【缓存击穿】 public <T,ID> T queryWithMutex(String keyPrefix,ID id, Class<T> type, Function<ID, T> 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. 实现缓存重建 // 4.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; T t = 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 成功, // 4.4.1 (获取锁成功应该再次检测redis缓存是否存在,做DoubleCheck。如果存在,则无需重建缓存。) // 4.4.2 根据id查询数据库 t = dbFallback.apply(id); // 模拟重建的延时 Thread.sleep(200); // 5.不存在,返回错误 if(t == null){ // 【缓存穿透】将空值写入redis stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回错误信息 return null; } // 6.存在,写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(t), time, unit); } catch (InterruptedException e) { throw new RuntimeException(); } finally { // 7. 释放互斥锁 unlock(lockKey); } // 8. 返回 return t; } // 获取 锁 private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.MINUTES); return BooleanUtil.isTrue(flag); } // 释放 锁 private void unlock(String key){ stringRedisTemplate.delete(key); } }
使用实现:
为解决缓存问题,数据量较小时可以使用互斥锁来解决。
Redis 全局id生成器:
自己写了一个Redis全局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; /** * Redis ID 生成器 */ @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; } }
测试:
@Resource private RedisIdWorker redisIdWorker; private ExecutorService es = Executors.newFixedThreadPool(500); @Test void testIdWorker() throws InterruptedException { CountDownLatch latch = new CountDownLatch(300); Runnable task = () -> { for (int i = 0; i < 100; i++){ long id = redisIdWorker.nextId("order"); System.out.println("id = "+ id); } latch.countDown(); }; long begin = System.currentTimeMillis(); for (int i = 0; i < 300; i++) { es.submit(task); } latch.await(); long end = System.currentTimeMillis(); System.out.println("time = " + (end - begin)); }
添加优惠券:
@Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override @Transactional public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); //2.判断秒杀是否开始 if(voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀尚未开始!"); } //3.判断秒杀是否已经结束 if(voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束!"); } //4.判断库存是否充足 if(voucher.getStock() < 1){ return Result.fail("库存不足!"); } //5.扣减库存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId).update(); if(!success){ //扣减失败 return Result.fail("库存不足!"); } //6.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //6.1 订单id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //6.2 用户id Long userId = UserHolder.getUser().getId(); voucherOrder.setUserId(userId); //6.3 代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //7.返回订单id return Result.ok(orderId); }
CAS法:
乐观锁不是锁,是一个解决线程安全问题的方法,如上,改变sql语句来实现乐观锁。
依赖:
@Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); //2.判断秒杀是否开始 if(voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀尚未开始!"); } //3.判断秒杀是否已经结束 if(voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束!"); } //4.判断库存是否充足 if(voucher.getStock() < 1){ return Result.fail("库存不足!"); } Long userId = UserHolder.getUser().getId(); synchronized(userId.toString().intern()) { // 获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { //5 一人一单 Long userId = UserHolder.getUser().getId(); //5.1 查询订单 Long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); //5.2 判断是否存在 if (count > 0) { //已买 return Result.fail("用户已经购买过一次!"); } //6.扣减库存 【乐观锁】 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") //set stock = stock - 1 .eq("voucher_id", voucherId) .gt("stock", 0) //where id = ? and stock > 0 .update(); if (!success) { //扣减失败 return Result.fail("库存不足!"); } //7.创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //7.1 订单id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2 用户id voucherOrder.setUserId(userId); //7.3 代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回订单id return Result.ok(orderId); }
基本原理:
基于Redis的分布式锁:
基于Redis实现分布式锁初级版本:
原子性:
SimpleRedisLock:
package com.hmdp.utils; import cn.hutool.core.lang.UUID; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.util.Collections; import java.util.concurrent.TimeUnit; 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) + "-"; 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 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() { // 调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId()); } /*@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); } }*/ }
依赖:
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
RedissonConfig:
package com.hmdp.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient(){ //配置 Config config = new Config(); config.useSingleServer().setAddress("redis://114.115.208.175"); // 创建RedissonClient 对象 return Redisson.create(config); } }
在下面完整展现
在下面完整展现
代码完整实现:
@Resource private IUserService userService; @Resource private StringRedisTemplate stringRedisTemplate; @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.queryBlogUser(blog); this.isBlogLiked(blog); }); return Result.ok(records); } /** * 点赞功能 * @param id * @return */ @Override public Result likeBlog(Long id) { // 1. 获取登录用户 Long userId = UserHolder.getUser().getId(); // 2.判断当前用户是否已经点赞 String key = BLOG_LIKED_KEY + id; Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); if(score == null){ // 3.如果未点赞,可以点赞 // 3.1.数据库点赞数 + 1 boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update(); // 3.2.保存用户到Redis的set集合中 zadd key value score if(isSuccess){ stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); } }else{ // 4.如果已经点赞,取消点赞 // 4.1 数据库点赞数 boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update(); // 4.2 把用户从redis的set集合中移除 if(isSuccess){ stringRedisTemplate.opsForZSet().remove(key, userId.toString()); } } return Result.ok(); } /** * 显示blog * @param id * @return */ @Override public Result queryBlogById(Long id) { // 1. 查询blog Blog blog = getById(id); if(blog == null){ return Result.fail("笔记不存在!"); } // 2. 查询blog有关的用户 queryBlogUser(blog); // 3. 查询bolg是否被点赞 isBlogLiked(blog); return Result.ok(blog); } /** * 展示点赞的前5位人 点赞排行榜 * @param id * @return */ @Override public Result queryBlogLikes(Long id) { String key = BLOG_LIKED_KEY + id; // 1.查询top5 的点赞用户 zrange key 0 4 Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); if(top5 == null || top5.isEmpty()){ return Result.ok(Collections.emptyList()); } // 2.解析出其中的id List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); String idStr = StrUtil.join(",", ids); // 3.根据用户id查询用户 / 转成UserDTO / WHERE id IN (5,1) ORDER BY FIELD(id,5,1) List<UserDTO> userDTOS = userService.query() .in("id",ids) .last("ORDER BY FIELD(id," + idStr +")").list() .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); // 4.返回 return Result.ok(userDTOS); } private void isBlogLiked(Blog blog) { UserDTO user = UserHolder.getUser(); if(user == null){ // 用户未登录,无需查询是否点赞 return; } // 1. 获取登录用户 Long userId = user.getId(); // 2.判断当前用户是否已经点赞 String key = BLOG_LIKED_KEY + blog.getId(); Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); blog.setIsLike(score != null); } private void queryBlogUser(Blog blog) { Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); }
@Resource private StringRedisTemplate stringRedisTemplate; /** * 判断是否关注还是取关 */ @Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; // 1.判断到底是关注还是取关 if(isFollow){ // 2.关注,新增数据 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess = save(follow); if(isSuccess){ // 把关注用户的id,放入redis的set集合中,sadd userId followUserId stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } }else { // 3.取关,删除 boolean isSuccess = remove(new QueryWrapper<Follow>() .eq("user_id", userId) .eq("follow_user_id", followUserId)); if(isSuccess){ // 把关注用户的id从Redis 集合中移除出去 stringRedisTemplate.opsForSet().remove(key, followUserId.toString()); } } return Result.ok(); } /** * 查询是否关注 */ @Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); // 1.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ? Integer count = Math.toIntExact(query().eq("user_id", userId).eq("follow_user_id", followUserId).count()); return Result.ok(count > 0); } }
Redis 中set方法,SINTER 可以取到交集。
@Override public Result followCommons(Long id) { // 1.获取当前用户 Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + userId; // 2.求交集 String key2 = "follows:" + id; Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); if(intersect == null || intersect.isEmpty()){ // 无交集 return Result.ok(Collections.emptyList()); } // 3.解析id List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); // 4.查询用户 List<UserDTO> users = userService.listByIds(ids) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(users); }
Feed流
拉模式:
推模式:
推拉结合模式:
优点:
此项目中用户量少,使用推模式:
/** * 保存笔记,并推送给粉丝 * @param blog * @return */ @Override public Result saveBlog(Blog blog) { // 1.获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 2.保存探店博文 boolean isSuccess = save(blog); if(!isSuccess){ return Result.fail("新增笔记失败!"); } // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ? List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); // 4.推送笔记id给所有粉丝 for(Follow follow :follows){ // 4.1 获取粉丝id Long userId = follow.getUserId(); // 4.2 推送 String key = FEED_KEY + userId ; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } // 3.返回id return Result.ok(blog.getId()); }
/** * feed流 滚动分页 * @param max * @param offset * @return */ @Override public Result queryBlogOfFollow(Long max, Integer offset) { // 1.获取当前用户 Long userId = UserHolder.getUser().getId(); // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count String key = FEED_KEY + userId; Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); // 3.判断是否为空 if(typedTuples == null || typedTuples.isEmpty()){ return Result.ok(); } // 4.解析数据:blogId 、 minTime(时间戳)、offset List<Long> ids = new ArrayList<>(typedTuples.size()); long minTime = 0; int os = 1; for (ZSetOperations.TypedTuple<String> tuple: typedTuples) { // 4.1 获取id ids.add(Long.valueOf(tuple.getValue())); // 4.2 获取分数(时间戳) long time = tuple.getScore().longValue(); if (time == minTime){ os++; }else{ minTime = time; os = 1; } } // 5.根据id查询blog String idStr = StrUtil.join(",", ids); List<Blog> blogs = query().in("id", ids) .last("ORDER BY FIELD(id," + idStr + ")").list(); for (Blog blog: blogs) { // 5.1.查询blog有关的用户 queryBlogUser(blog); // 5.2.查询bolg是否被点赞 isBlogLiked(blog); } // 6.封装并返回 ScrollResult r = new ScrollResult(); r.setList(blogs); r.setOffset(os); r.setMinTime(minTime); return Result.ok(r); }
/** * 附近商铺 * @param typeId * @param current * @param x * @param y * @return */ @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 // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE String key = SHOP_GEO_KEY + typeId; GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() .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); }
/** * 签到功能 */ @Override public Result sign() { // 1.获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 2.获取日期 LocalDateTime now = LocalDateTime.now(); // 3.拼接key String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy-MM")); 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(); }
/** * 连续签到统计功能 * @return */ @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.获取本月截至今天为止的所有的签到记录,返回的是一个十进制的数字 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位// 6.2 判断这个bit位是否为0 if((num & 1) == 0){ // 6.3 如果为0,说明未签到,结束 break; }else{ // 6.4 如果不为0,说明已签到,计数器+1 count++; } // 6.5 把数字右移一位,抛弃最后一个bit位,继续下一个bit位。 num >>>=1; } return Result.ok(count); }
此篇文章仅供大家学习参考,有问题可以私信我哦…
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。