赞
踩
@Override public Result sendCode(String phone, HttpSession session) { // 1. 校验手机号,不存在返回错误信息 if (RegexUtils.isPhoneInvalid(phone)){ // 不符合返回错误信息 return Result.fail("手机号格式不正确"); } // 2.生成验证码 String code = RandomUtil.randomNumbers(6); // 3.保存验证码存入session session.setAttribute("code",code); // 4.发送验证码 log.debug("发送短信验证码成功,验证码为{}",code); // 5.返回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. 校验验证码 String cacheCode = (String) session.getAttribute("code"); String code = loginForm.getCode(); if (StringUtils.isEmpty(cacheCode) || !cacheCode.equals(code)){ // 3. 验证码不一致,报错 return Result.fail("验证码错误"); } // 4. 判断用户是否存在 User user = query().eq("phone", phone).one(); // 5. 不存在则创建用户并保存数据库 if (user == null){ user = createUserWithPhone(phone); } // 6. 把登录信息存入session当中 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); session.setAttribute("user",userDTO); return Result.ok(); }
拦截器配置
3.1 编写拦截类
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); throw new RuntimeException("用户没有登录"); } // 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(); } }
3.2 注册拦截器
@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ); } }
session短信登录存在的问题,当多台tomcat部署的时候,每台tomcat都会有自己的session,由于负载均衡会轮询多台tomcat,会导致数据丢失问题。例如:验证码存在第一天tomcat上,而登录是访问的是第二天tomcat上,这样无法找到验证码信息。
public Result sendCode(String phone, HttpSession session) {
// 1. 校验手机号,不存在返回错误信息
if (RegexUtils.isPhoneInvalid(phone)){
// 不符合返回错误信息
return Result.fail("手机号格式不正确");
}
// 2.生成验证码
String code = RandomUtil.randomNumbers(6);
// 3.保存验证码存入redis,要保证key唯一,所以使用phone作为key,并设置过期时间为2分钟
redisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 4.发送验证码
log.debug("发送短信验证码成功,验证码为{}",code);
// 5.返回OK
return Result.ok();
}
public Result login(LoginFormDTO loginForm, HttpSession session) { // 1. 校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ // 不符合返回错误信息 return Result.fail("手机号格式不正确"); } // 2. 校验验证码,从redis当中取验证码校验 String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone); String code = loginForm.getCode(); if (StringUtils.isEmpty(cacheCode) || !cacheCode.equals(code)){ // 3. 验证码不一致,报错 return Result.fail("验证码错误"); } // 4. 判断用户是否存在 User user = query().eq("phone", phone).one(); // 5. 不存在则创建用户并保存数据库 if (user == null){ user = createUserWithPhone(phone); } // 6.将登录信息保存到Redis当中 // 6.1 随机生成 token作为key String token = UUID.randomUUID().toString(true); // 6.2 将user对象转为HashMap存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().ignoreNullValue().setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString()));//为了解决Inter->String 的问题 // 6.3 存入Redis并设置过期时间 String key = LOGIN_USER_KEY + token; redisTemplate.opsForHash().putAll(key,userMap); redisTemplate.expire(key,LOGIN_USER_TTL,TimeUnit.MINUTES); // 7.返回token给前端 return Result.ok(token); }
配置拦截器
3.1 配置刷新缓存时间拦截器
private StringRedisTemplate redisTemplate; public RefreshInterceptor(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 获取请求头中的token String token = request.getHeader("authorization"); if (StringUtils.isEmpty(token)){ // 2. 不存在就交给登录拦截器 return true; } // 3. 基于token获取用户 String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key); if (userMap.isEmpty()){ // 4. 不存在就交给登录拦截器 return true; } UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 5. 存在就保存到ThreadLocal当中 UserHolder.saveUser(userDTO); // 6. 刷新token有效期 redisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES); // 7. 放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); }
3.2 配置登录拦截器
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从ThreadLocal获取用户
UserDTO userDTO = UserHolder.getUser();
if (userDTO == null){
// 2. 如果ThreadLocal中没有用户,那就拦截
throw new RuntimeException("用户未登录");
}
return true;
}
@Override public Result queryById(Long id) { // 1. 从redis中查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (!StringUtils.isEmpty(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(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); }
待完成 对商品分类列表进行缓存处理
查询数据
@Override public Result queryById(Long id) { // 1. 从redis中查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (!StringUtils.isEmpty(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(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); }
更新数据
@Override
@Transactional // 事务是为了保证Redis和MySQL的原子性
public Result updateShop(Shop shop) {
Long id = shop.getId();
if (id == null){
return Result.fail("店铺不存在");
}
// 1. 更新数据库
updateById(shop);
// 2. 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
缓存穿透是指:大量请求缓存和数据库中不存在的数据,导致数据库压力过大。
使用空对象实现,防止缓存穿透
@Override public Result queryById(Long id) { // 1. 从redis中查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StringUtils.hasText(shopJson)) { // 3. 存在,直接返回 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } // 4. 判断是否为空值 if (shopJson != null){ return Result.fail("系统繁忙,请重试"); } // 5. 不存在,根据id查询数据库 Shop shop = getById(id); // 6. 不存在,返回错误 if (shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商品不存在"); } // 7. 存在,存入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); }
互斥锁解决缓存击穿
public Shop queryWithMutex(Long id) { // 1. 从redis中查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StringUtils.hasText(shopJson)) { // 3. 存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } // 4. 判断是否为空值 if (shopJson != null) { return null; } // 5. 实现缓存重建 String lockKey = "lock:shop:" + id; Shop shop = null; try { // 5.1 获取互斥锁 boolean isLock = tryLock(lockKey); // 5.2 判断是否获取成功 if (!isLock) { // 5.3 失败,休眠并重试 Thread.sleep(50); return queryWithMutex(id); } // 5.4 成功,再次查询缓存,有可能在加锁的时候,别的线程已经重建缓存了 shopJson = stringRedisTemplate.opsForValue().get(key); if (StringUtils.hasText(shopJson)) { // 3. 存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } // 5.5 别的线程没有重建缓存,查询数据库 Thread.sleep(100); shop = getById(id); // 6. 不存在,返回错误 if (shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 7. 存在,存入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { // 8. 释放互斥锁 unLock(lockKey); } return shop; }
逻辑删除实现缓存击穿
public Shop queryWithLogicalExpire(Long id) { // 1. 从redis中查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StringUtils.isEmpty(shopJson)) { // 3. 不存在,直接返回 return null; } // 4. 命中,把json反序列化成对象 RedisData redisData; redisData = JSONUtil.toBean(shopJson, RedisData.class); JSONObject jsonObject = (JSONObject) redisData.getData(); Shop shop = JSONUtil.toBean(jsonObject, Shop.class); LocalDateTime expireTime = redisData.getExpireTime(); // 5. 判断是否过期 if (expireTime.isAfter(LocalDateTime.now())) { // 5.1 未过期,直接返回店铺信息 return shop; } // 5.2 已过期,需要缓存重建 // 6. 进行缓存重建 // 6.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; // 6.2 判断是否获取锁成功 boolean isLock = tryLock(lockKey); if (isLock) { // 再次检测 shopJson = stringRedisTemplate.opsForValue().get(key); redisData = JSONUtil.toBean(shopJson, RedisData.class); jsonObject = (JSONObject) redisData.getData(); shop = JSONUtil.toBean(jsonObject, Shop.class); expireTime = redisData.getExpireTime(); // 判断是否过期 if (expireTime.isAfter(LocalDateTime.now())) { // 未过期,直接返回店铺信息 return shop; } // 6.3 成功开启新线程执行缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { // 重建缓存 try { this.saveShop2Redis(id, 20L); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放所 unLock(lockKey); } }); } return shop; } public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException { // 1. 查询店铺信息 Shop shop = getById(id); Thread.sleep(50); // 2. 封装逻辑过期时间 RedisData redisData = new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 3. 写入Redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData)); } private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); }
public class CacheClientUtils { private final StringRedisTemplate stringRedisTemplate; private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public CacheClientUtils(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 存储任意对象至redis,解决了缓存穿透问题 * @param key redis中的键 * @param value redis当中的值 * @param time 过期时间 * @param unit 过期时间的单位 */ public void setWithPassThrough(String key, Object value, Long time, TimeUnit unit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit); } /** * 存储任意对象至redis,解决了缓存击穿问题 * @param key redis中的键 * @param value redis当中的值 * @param time 过期时间 * @param 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))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } /** * 在缓存穿透的情况下,查询数据 * @param keyPrefix key的前缀 * @param id 查询的id * @param type 查询对象的类型 * @param dbFallback 查询方法 * @param time 过期时间 * @param unit 过期时间单位 * @return */ public <R,ID> R queryWithPassThough(String keyPrefix, ID id, Class<R> type, Function<ID, R> 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); } // 4. 判断是否为空值 if (json != null) { return null; } // 5. 不存在,根据id查询数据库 R r = dbFallback.apply(id); // 6. 不存在,返回错误 if (r == null) { stringRedisTemplate.opsForValue().set(key, "", time, unit); return null; } // 7. 存在,存入redis this.setWithPassThrough(key,r,time,unit); return r; } /** * 热点key问题,在缓存击穿的情况下,查询数据 * @param keyPrefix key的前缀 * @param id 查询的id * @param type 查询对象的类型 * @param dbFallback 查询方法 * @param time 过期时间 * @param unit 过期时间单位 */ public<R,ID> R queryWithLogicalExpire(String keyPrefix,ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { // 1. 从redis中查询商铺缓存 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StrUtil.isBlank(json)) { // 3. 不存在,直接返回 return null; } // 4. 命中,把json反序列化成对象 RedisData redisData; redisData = JSONUtil.toBean(json, RedisData.class); JSONObject jsonObject = (JSONObject) redisData.getData(); R r = JSONUtil.toBean(jsonObject, 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; // 6.2 判断是否获取锁成功 boolean isLock = tryLock(lockKey); if (isLock) { // 再次检测 json = stringRedisTemplate.opsForValue().get(key); redisData = JSONUtil.toBean(json, RedisData.class); jsonObject = (JSONObject) redisData.getData(); r = JSONUtil.toBean(jsonObject, type); expireTime = redisData.getExpireTime(); // 判断是否过期 if (expireTime.isAfter(LocalDateTime.now())) { // 未过期,直接返回店铺信息 return r; } // 6.3 成功开启新线程执行缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { // 重建缓存 try { R r1 = dbFallback.apply(id); this.setWithLogicalExpire(key, r1, time,unit); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放所 unLock(lockKey); } }); } return r; } private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); } }
@Component public class RedisIdWorker { private static final long BEGIN_TIMESTAMP = 1640995200L; private final StringRedisTemplate stringRedisTemplate; private final static Long COUNT_BITS = 32L; public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 生成全局ID * @param keyPrefix 键前缀 * @return */ 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("yyyyy:MM:dd")); long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); return timestamp<<COUNT_BITS|count; } }
@Override @Transactional public Result seckillVoucher(Long voucherId) { // 1. 查询优惠券 SeckillVoucher voucher = voucherService.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 = voucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update(); if (! success){ // 扣除失败 return Result.fail("秒杀券已抢完"); } // 6. 创建订单 // 6.1 订单id VoucherOrder voucherOrder = new VoucherOrder(); 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(); }
如果只是采用普通的方式,会有超卖的问题发生,总共只有100个票,但是有200个线程去并发访问。按照道理来说,异常率应该是50%,而此时小于了50%,所以出现了超卖现象。
乐观锁只能针对与更新操作,对于插入操作无法使用乐观锁。
@Override @Transactional public Result seckillVoucher(Long voucherId) { // 1. 查询优惠券 SeckillVoucher voucher = voucherService.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 = voucherService.update().setSql("stock = stock -1") .eq("voucher_id", voucherId) .eq("stock",voucher.getStock()) // 重点是这里,添加了乐观锁 .update(); if (! success){ // 扣除失败 return Result.fail("秒杀券已抢完"); } // 6. 创建订单 // 6.1 订单id VoucherOrder voucherOrder = new VoucherOrder(); 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(); }
添加了乐观锁导致了成功率过低,异常率高于我们预计的50%,所以需要进行再次修改。
针对本次业务,由于库存天然需要大于0,所以进行再次修改
@Override @Transactional public Result seckillVoucher(Long voucherId) { // 1. 查询优惠券 SeckillVoucher voucher = voucherService.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 = voucherService.update().setSql("stock = stock -1") .eq("voucher_id", voucherId) .gt("stock",0) // 修改后的乐观锁 .update(); if (! success){ // 扣除失败 return Result.fail("秒杀券已抢完"); } // 6. 创建订单 // 6.1 订单id VoucherOrder voucherOrder = new VoucherOrder(); 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(); }
现在我们可以发现异常率是50%,可以很好的解决超卖的问题。
修改订单实现类
@Override public Result seckillVoucher(Long voucherId) { // 1. 查询优惠券 SeckillVoucher voucher = voucherService.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()) { // spring事务会失效,所以需要获取代理对象,来完成事务功能 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional @Override public Result createVoucherOrder(Long voucherId) { // 查询是否存在该用户的订单 Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("您已购买过该券"); } // 5. 扣除库存 boolean success = voucherService.update().setSql("stock = stock -1") .eq("voucher_id", voucherId) .gt("stock", 0) // 修改后的乐观锁 .update(); if (!success) { // 扣除失败 return Result.fail("秒杀券已抢完"); } // 6. 创建订单 // 6.1 订单id VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 6.2 用户id voucherOrder.setUserId(userId); // 6.3 代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); // 7. 返回订单id return Result.ok(); }
导入aspectj
依赖
<!--aspectj的依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
在启动类中,暴露aspectj
的代理对象
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露aspectj的代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
配置集群环境
配置前端代理
集群模式下,会出现错误,每个服务器相当于一个进程,每个进程会有自己的JVM
。而synchronized
是JVM
实现的,所以两个服务器没有共用锁,导致并发问题的发生。
public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate redisTemplate; private static final String KEY_PREFIX = "lock:"; public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) { this.name = name; this.redisTemplate = redisTemplate; } @Override public boolean tryLock(long timeoutSec) { long threadId = Thread.currentThread().getId(); Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unlock() { redisTemplate.delete(KEY_PREFIX + name); } }
问题:
上面的这种实现,会出现误删的效果。例如,线程1
业务执行时间较长,超过了设置的超时时间,导致了,业务还没执行完,锁就自动释放了。此时,线程2
尝试获取锁,由于线程1
已经释放了锁,所以线程2
可以获取锁,线程2
此时执行自己的业务,而恰好线程1
业务完成释放锁,造成了线程1
释放了线程2
的锁
修改后的版本,redis
锁
public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate redisTemplate; private static final String KEY_PREFIX = "lock:"; private static final String ID_PREFIX = UUID.randomUUID().toString(true); public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) { this.name = name; this.redisTemplate = redisTemplate; } @Override public boolean tryLock(long timeoutSec) { // 1.存入锁的标识 String threadId = ID_PREFIX+Thread.currentThread().getId(); // 2.是否获取成功 Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unlock() { // 1.从redis中获取锁标识 String s = redisTemplate.opsForValue().get(KEY_PREFIX + name); // 2. 判断标识是否相等 String threadId = ID_PREFIX+Thread.currentThread().getId(); if (threadId.equals(s)){ redisTemplate.delete(KEY_PREFIX + name); } } }
由于判断锁和释放锁不是原子性的操作,可能还会导致并发问题的出现。
Java
调用lua
脚本
lua
脚本,位置在resource
目录下
-- 比较线程标识与锁中标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁
return redis.call('del', KEYS[1])
end
return 0
ILock
实现类
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 redisTemplate; 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")); // 获取lua脚本 UNLOCK_SCRIPT.setResultType(Long.class); // 设置lua脚本的返回值类型 } public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) { this.name = name; this.redisTemplate = redisTemplate; } @Override public boolean tryLock(long timeoutSec) { // 1.存入锁的标识 String threadId = ID_PREFIX+Thread.currentThread().getId(); // 2.是否获取成功 Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unlock() { // 调用lua脚本 redisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX+Thread.currentThread().getId() ); } }
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
redisson
配置文件@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
// 配置类
Config config = new Config();
// 添加redis地址,这里采用单机redis,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.88.130:6379").setPassword("123456");
// 创建客户端
return Redisson.create(config);
}
}
@Autowired private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { // 获取可重入锁,指定锁的名称 RLock lock = redissonClient.getLock("anyLock"); // 尝试获取锁,获取锁的最大等待时间(期间重试) 锁自动释放时间 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 判断是否获取成功 if (isLock) { try { System.out.println("执行业务"); } finally { // 释放锁 lock.unlock(); } } }
获取锁的流程
释放所的流程
通过测试,验我们的流程
private RLock lock ; @BeforeEach void setup(){ lock = redissonClient.getLock("order"); } @Test void method1() { // 尝试获取锁 boolean isLock = lock.tryLock(); if (!isLock){ log.error("获取锁失败...1"); } try{ log.info("获取锁成功...1"); method2(); log.info("开始执行业务...1"); }finally { log.warn("准备释放锁...1"); lock.unlock(); } } void method2(){ // 尝试获取锁 boolean isLock = lock.tryLock(); if (!isLock){ log.error("获取锁失败...2"); } try{ log.info("获取锁成功...2"); log.info("开始执行业务...2"); }finally { log.warn("准备释放锁...2"); lock.unlock(); } }
执行到method1
获取锁之后和我们的猜想是一样的通过Hash
进行存储
超时等待原理
1. 异步查询锁的剩余时间
2. 判断设定时间是否大于(查询消耗时间+消耗时间)
- 如果小于 返回 false
- 如果大于 那么会订阅锁的释放消息,在锁释放的通知线程。在此期间线程会进行等待,等待时间为设定时间的剩余时间,
- 如果超过设定时间会返回false
- 如果没超过,会进入循环一直重试,尝试获取锁
超时释放原理
1. redisson内部设置了一个定时任务,每隔设定时间的1/3,进行一次时间的刷新,这样就可以保证任务执行完成。(自己如果设定了超时时间,那么不会有定时任务)
2. 在释放锁的时候,也会把定时任务给释放掉
现在的流程
修改VoucherServiceImpl.java
文件
@Service public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService { @Resource private ISeckillVoucherService seckillVoucherService; @Autowired private StringRedisTemplate redisTemplate; @Override public Result queryVoucherOfShop(Long shopId) { // 查询优惠券信息 List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId); // 返回结果 return Result.ok(vouchers); } @Override @Transactional public void addSeckillVoucher(Voucher voucher) { // 保存优惠券 save(voucher); // 保存秒杀信息 SeckillVoucher seckillVoucher = new SeckillVoucher(); seckillVoucher.setVoucherId(voucher.getId()); seckillVoucher.setStock(voucher.getStock()); seckillVoucher.setBeginTime(voucher.getBeginTime()); seckillVoucher.setEndTime(voucher.getEndTime()); seckillVoucherService.save(seckillVoucher); // 保存优惠券到数据库的时候,同时保存到redis,通过redis来判断库存和一人一单 redisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString()); } }
新建lua
脚本
-- seckill.lua -- 1.参数列表 -- 1.1.优惠券id local voucherId = ARGV[1] -- 1.2.用户id local userId = ARGV[2] -- 2.数据歌ey -- 2.1.库存ey 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.3.判断用户是否下单SISMEMBER orderKey userId if (redis.call('sismember', orderKey, userId) == 1) then --3.3.存在,说明是重复下单,返回2 return 2 end -- 3.4. 扣库存 redis.call('incrby', stockKey, -1) -- 3.5. 下单保存用户 redis.call("sadd", orderKey, userId) return 0
修改VoucherOrderServiceImpl.java
文件
@Service @Slf4j public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Autowired private ISeckillVoucherService voucherService; @Autowired private RedisIdWorker redisIdWorker; @Autowired private StringRedisTemplate redisTemplate; private final BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024*1024); private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); private static final DefaultRedisScript<Long> SECKILL_SCRIPT; static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); // 获取lua脚本 SECKILL_SCRIPT.setResultType(Long.class); // 设置lua脚本的返回值类型 } @PostConstruct // @PostConstruct修饰的方法会在类初始化之后执行 private void init(){ SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } private class VoucherOrderHandler implements Runnable{ @Override public void run() { while (true){ try { // 1. 获取队列中的订单信息 VoucherOrder voucherOrder = orderTask.take(); // 2. 创建订单 handleVoucherOrder(voucherOrder); } catch (InterruptedException e) { log.error("创建订单时出现异常",e); } } } } private IVoucherOrderService proxy; private void handleVoucherOrder(VoucherOrder voucherOrder) { // 1.无法从UserHolder当中获取userID,该函数在子线程当中运行 Long userId =voucherOrder.getUserId(); // 其实我感觉这里不需要再考虑并发的情况了,并发情况已经被Redis处理了 SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:"+userId,redisTemplate); boolean isLock = simpleRedisLock.tryLock(1200); if (!isLock) { log.error("一个用户只允许抢一个优惠券"); return; } try { proxy.createVoucherOrder(voucherOrder); }finally { simpleRedisLock.unlock(); } } @Override public Result seckillVoucher(Long voucherId) { // 需要对秒杀时间进行判断! // 获取用户 Long userId = UserHolder.getUser().getId(); Long result = redisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), // 没有key类型的参数,不能传null,要传空集合 voucherId.toString(), userId.toString() ); // 2. 判断结果是否为0 int r = result.intValue(); if (r != 0){ // 2.1 不为0,没有购买资格 return Result.fail(r == 1 ? "库存不足":"不能重复下单"); } // 2.2 为0,有购买资格,将信息保存到阻塞队列里面 // TODO 保存阻塞队列 long orderId = redisIdWorker.nextId("order:"); // 3. 准备订单信息 VoucherOrder voucherOrder = new VoucherOrder(); voucherOrder.setId(orderId); // 3.1 用户id voucherOrder.setUserId(userId); // 3.2 代金券id voucherOrder.setVoucherId(voucherId); // 代理对象现如今也无法获取,代理对象也是存储在ThreadLocal当中 proxy = (IVoucherOrderService) AopContext.currentProxy(); orderTask.add(voucherOrder); // 把代理对象放到成员变量当中 // 4. 返回订单id return Result.ok(orderId); } @Transactional @Override public void createVoucherOrder(VoucherOrder voucherOrder) { // 查询是否存在该用户的订单 Long userId = voucherOrder.getUserId(); Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count(); if (count > 0) { log.error("您已购买过该券"); return ; } // 5. 扣除库存 boolean success = voucherService.update().setSql("stock = stock -1") .eq("voucher_id", voucherOrder) .gt("stock", 0) // 修改后的乐观锁 .update(); if (!success) { // 扣除失败 log.error("秒杀券已抢完"); return; } // 6. 将订单保存至数据库 save(voucherOrder); } }
秒杀优化的思路是什么?
- 先利用Redis完成库存余量、一人一单判断,完成抢单业务
- 再将下单业务放入阻塞队列,利用独立线程异步下单
基于阻塞队列的异步秒杀存在哪些问题?
- 内存限制问题
- 数据安全问题
Redis
实现消息队列的三种方式
list
结构:基于List结构模拟消息队列PubSub
:基本的点对点消息模型Stream
:比较完善的消息队列模型List
消息队列List
消息队列的优缺点
优点:
Redis
存储,不受限于VM内存上限Redis
的持久化机制,数据安全性有保证缺点:
PubSubd
消息队列优点:
缺点:
Stream
的消息队列在SystemConstants.java
中修改上传文件路径
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "E:\\redis-code\\nginx-1.18.0\\html\\hmdp\\imgs\\"; // 修改这一行的文件路径,如果有OSS服务器,可以自己重新实现该接口
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;
}
在BlogServiceImpl.java中修改文件
@Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Autowired private IUserService userService; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public Result queryBlogById(Long id) { Blog blog = getById(id); if (blog == null){ return Result.fail("笔记不存在"); } queryBlogUser(blog); return Result.ok(blog); } private void queryBlogUser(Blog blog) { Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); } }
修改实体类blog.java
@Data @Accessors(chain = true) @TableName("tb_blog") public class Blog implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 商户id */ private Long shopId; /** * 用户id */ private Long userId; /** * 用户图标 */ @TableField(exist = false) private String icon; /** * 用户姓名 */ @TableField(exist = false) private String name; /** * 是否点赞过了 */ @TableField(exist = false) private Boolean isLike; /** * 标题 */ private String title; /** * 探店的照片,最多9张,多张以","隔开 */ private String images; /** * 探店的文字描述 */ private String content; /** * 点赞数量 */ private Integer liked; /** * 评论数量 */ private Integer comments; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; }
修改实现类BlogServiceImpl.java
,在实现类中要实现两个功能
Redis
当中liked
字段进行修改Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Autowired private IUserService userService; @Autowired 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.isBlogLike(blog); }); return Result.ok(records); } @Override public Result queryBlogById(Long id) { Blog blog = getById(id); if (blog == null){ return Result.fail("笔记不存在"); } queryBlogUser(blog); isBlogLike(blog); return Result.ok(blog); } private void isBlogLike(Blog blog) { // 1. 获取用户id if (UserHolder.getUser() == null){ return; } Long userId = UserHolder.getUser().getId(); // 2. 判断用户是否已经点赞了 String key = BLOG_LIKED_KEY + blog.getId(); Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); blog.setIsLike(score != null); } @Override public Result likeBlog(Long id) { // 1. 获取用户id Long userId = UserHolder.getUser().getId(); // 2. 判断用户是否已经点赞了 String key = BLOG_LIKED_KEY+ id; Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); if (score == null){ // 2.1 如果未点赞,那么数据库的点赞数+1,并将其存入redis集合 boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update(); if (isSuccess){ stringRedisTemplate.opsForZSet().add(key, String.valueOf(userId),System.currentTimeMillis()); } }else { // 2.2 如果点赞,那么数据库的点赞数-1,并将其移除redis集合 boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update(); if (isSuccess){ stringRedisTemplate.opsForZSet().remove(key, String.valueOf(userId)); } } return Result.ok(); } @Override public Result queryBlogLikes(Long id) { // 1. 查询top5点赞数 zrange key 0 4 String key = BLOG_LIKED_KEY+ id; // 2. 我只用两个用户查过,发现是有序的,感觉内部实现应该采用的TreeSet Set<String> userIds = stringRedisTemplate.opsForZSet().range(key, 0, 4); if (userIds == null || userIds.isEmpty()){ return Result.ok(Collections.emptyList()); } List<User> users = new ArrayList<>(5); // 分别对每一个id进行查询,我看视频里面是通过自定义排序做的,我觉得太麻烦了,就分别查询了。 userIds.forEach(userId->{ users.add(userService.getById(userId)); }); List<UserDTO> userDTOS = users .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS); } private void queryBlogUser(Blog blog) { Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); } }
在FollowServiceImpl.java
当中,有两个方法,follo()
用来关注或者取消关注,isFollow()
用来判断是否关注了目标
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private IUserService userService; @Override public Result follow(Long followUserId, Boolean isFollow) { // 1. 获取登录用户 Long userId = UserHolder.getUser().getId(); // 2. 判断是关注还是取关 if (isFollow){ // 2. 关注 Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); boolean isSuccess = save(follow); if (isSuccess){ String key = "follow:"+userId; stringRedisTemplate.opsForSet().add(key, String.valueOf(followUserId)); } }else { LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getFollowUserId, followUserId); queryWrapper.eq(Follow::getUserId, userId); boolean isSuccess = remove(queryWrapper); if (isSuccess){ String key = "follow:"+userId; stringRedisTemplate.opsForSet().remove(key); } } return Result.ok(); } @Override public Result isFollow(Long followUserId) { // 1. 获取登录用户 Long userId = UserHolder.getUser().getId(); // 2.查询是否关注该用户 LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getFollowUserId, followUserId); queryWrapper.eq(Follow::getUserId, userId); // 3. 判断是否大于0 int count = count(queryWrapper); return Result.ok(count>0); } }
把关注列表存在Redis
的Set
当中,通过 进行交集,就可以查找共同关注。
修改FollowServiceImpl.java
@Override public Result followCommon(Long id) { // 1. 获取当前用户 Long userId = UserHolder.getUser().getId(); // 2. 获取当前用户关注列表和目标用户关注列表 Set<String> followIds = stringRedisTemplate.opsForSet().intersect(userId.toString(), id.toString()); if (followIds == null || followIds.isEmpty()){ return Result.ok(Collections.emptyList()); } List<UserDTO> userDTOS = userService.listByIds(followIds) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS); }
千万粉丝才考虑使用推拉结合,一般采用推模式就可以了。
在保存博客的时候,将id
发送到每个粉丝发件箱,修改BlogServiceImpl.java
的保存博客方法
@Override public Result saveBlog(Blog blog) { // 获取用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 boolean isSuccess = save(blog); if (!isSuccess) { return Result.fail("保存失败"); } // 查询该用户所有的粉丝 LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getFollowUserId, user.getId()); List<Follow> followList = followService.list(queryWrapper); for (Follow follow : followList) { String key = "feed:"+follow.getUserId(); stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(),System.currentTimeMillis()); } return Result.ok(); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。