短信登录
将逻辑过期时间写入redis里面
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;@Resourceprivate IUserInfoService userInfoService;/*** 发送手机验证码*/@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {return userService.sedCode(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);}
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sedCode(String phone, HttpSession session) {//1. 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回错误信息return Result.fail("手机号格式错误");}//3. 符合,生成验证码String code = RandomUtil.randomNumbers(6);//4. 保存验证码到sessionsession.setAttribute("code",code);//5. 发送验证码log.debug("发送短信验证码成功,验证码:{}",code);//返回okreturn Result.ok();}@Overridepublic 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.一致,根据手机号查询用户User user = query().eq("phone", phone).one();//5. 判断用户是否存在if (user == null){//6. 不存在,创建新用户user = createUserWithPhone(phone);}//7.保存用户信息到sessionsession.setAttribute("user",BeanUtil.copyProperties(user,UserDTO.class));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;}
}
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 获取sessionHttpSession session = request.getSession();//2.获取session中的用户Object user = session.getAttribute("user");//3. 判断用户是否存在if (user == null){//4. 不存在,拦截response.setStatus(401);return false;}//5. 存在 保存用户信息到ThreadLocalUserHolder.saveUser((UserDTO) user);//6. 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}
表现层不变,业务层做一些改动,生成验证码存到redis里面,在登录校验的时候直接从redis里面读取验证码,再随机生成token,存入到redis里面
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sedCode(String phone, HttpSession session) {//1. 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回错误信息return Result.fail("手机号格式错误");}//3. 符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4 这里利用redis进行更改,保存验证码到redis : set key value ex 120stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//5. 发送验证码log.debug("发送短信验证码成功,验证码:{}",code);//返回okreturn Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1. 校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}//2. 校验验证码 从redis里面取验证码了String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)){//3. 不一致,报错return Result.fail("验证码错误");}//4.一致,根据手机号查询用户User user = query().eq("phone", phone).one();//5. 判断用户是否存在if (user == null){//6. 不存在,创建新用户user = createUserWithPhone(phone);}// 随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 将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()));//存储stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);//存数据时不能设置有效期,所以先存后设置stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);//返回tokenreturn Result.ok(token);}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;}
}
拦截器也变为了两个,一个负责拦截一切路径同时负责保存threadlocal以及刷新ttl操作,另一个负责查询用户是否存在,形成一个拦截器链:
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//在创建对象时将stringRedisTemplate传给拦截器使用//登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/user/login","/user/code","/shop-type/**","/upload/**").order(1);//刷新拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于TOKEN获取redis中的用户String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true;}// 5.将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否需要拦截if (UserHolder.getUser()==null) {//、没有 需要拦截response.setStatus(401);return false;}// 由用户,放行return true;}}
商户查询缓存
缓存:数据交换的缓冲区(Cache),存储临时数据,一般读写性能高
缓存作用:降低后端负载
提高读写效率,降低响应时间
缓存成本:数据一致性成本
代码维护成本
运维成本
根据id查询商户信息:
@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;//从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//不存在 根据id查询数据库Shop shop = getById(id);//数据库不存在,返回错误if (shop == null) return Result.fail("店铺不存在");//存在,先写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));//返回return Result.ok(shop);}
}
给店铺类型查询业务添加缓存
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryTypeList() {//先查询缓存 后面的名字是自己起的,是一个列表String shopJson = stringRedisTemplate.opsForValue().get("cache:shopType:list");//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在直接返回 返回一个listList<ShopType> list = JSONUtil.toList(shopJson, ShopType.class);return Result.ok(list);}//缓存为空则查询数据库 按照sort字段升序排序List<ShopType> shopTypeList = query().orderByAsc("sort").list();//数据库不存在返回错误if (shopTypeList == null || shopTypeList.isEmpty()) {return Result.fail("商铺类型不存在");}//数据库存在,先写入redisstringRedisTemplate.opsForValue().set("cache:shopType:list",JSONUtil.toJsonStr(shopTypeList));//返回return Result.ok(shopTypeList);}
}
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryTypeList() {String key = "cache:typelist";//1.在redis中间查询List<String> shopTypeList = new ArrayList<>();shopTypeList = stringRedisTemplate.opsForList().range(key, 0, -1);//2.判断是否缓存中了//3.中了返回if (!shopTypeList.isEmpty()) {List<ShopType> typeList = new ArrayList<>();for (String s : shopTypeList) {ShopType shopType = JSONUtil.toBean(s, ShopType.class);typeList.add(shopType);}return Result.ok(typeList);}//4.没中数据库中查List<ShopType> typeList = query().orderByAsc("sort").list();//5.不存在直接返回错误if(typeList.isEmpty()){return Result.fail("不存在分类");}for(ShopType shopType : typeList){String s = JSONUtil.toJsonStr(shopType);shopTypeList.add(s);}//6.存在直接添加进缓存stringRedisTemplate.opsForList().rightPushAll(key, shopTypeList);return Result.ok(typeList);}
}
@Transactionalpublic Result update(Shop shop) {Long id = shop.getId();if (id == null){return Result.fail("店铺id不能为空");}//更新数据库updateById(shop);//删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}
一般删除缓存,单体系统,将缓存与数据库放在同一个事务,分布式系统,利用TCC等分布式事务方案。权衡之下,先操作数据库,再删除缓存
缓存穿透
还可以:增加id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
public Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;//从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}查询命中的是否是空值if (shopJson != null){//返回错误信息return Result.fail("店铺不存在");}//不存在 根据id查询数据库Shop shop = getById(id);//数据库不存在,返回错误if (shop == null) {/将空值放入缓存stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return Result.fail("店铺不存在");}//存在,先写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);//返回return Result.ok(shop);}
缓存雪崩
缓存击穿
/*** 互斥锁解决缓存击穿* @param id* @return*/public Shop queryWithMutex(Long id){String key = CACHE_SHOP_KEY + id;//从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//查询命中的是否是空值if (shopJson != null){//返回错误信息return null;}//开始实现缓存重建//换取互斥锁String lockKey = LOCK_SHOP_KEY+id;Shop shop = null;try {boolean isLock = tryLock(lockKey);//判断是否获取成功if (!isLock){//失败则休眠并重试Thread.sleep(50);return queryWithMutex(id); //重试:递归}//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//查询命中的是否是空值if (shopJson != null){//返回错误信息return null;}//成功根据id查询数据库shop = getById(id);//数据库不存在,返回错误if (shop == null) {//将空值放入缓存stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}//存在,先写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//释放互斥锁unlock(lockKey);}//返回return shop;}
//过期时间类@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}
private void saveShop2Redis(Long id, Long expireSeconds){//查询店铺数据Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));}
@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate ShopServiceImpl shopService;@Testvoid testSaveShop(){shopService.saveShop2Redis(1L,10L);}
}
执行逻辑过期
/*** 逻辑过期* @param id* @return*/public Shop queryWithLogicalExpire(Long id){String key = CACHE_SHOP_KEY + id;//从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isBlank(shopJson)) {//未命中,直接返回return null;}//命中,需要判断过期时间//先把json反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);JSONObject data = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//判断是否过期if (expireTime.isAfter(LocalDateTime.now())){//未过期,直接返回店铺信息return shop;}//已过期,需要缓存重建//缓存重建//获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);//判断是否获取锁成功if (isLock){//成功 开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(()->{try {//重建缓存this.saveShop2Redis(id,20L);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁unlock(lockKey);}});}//返回商铺信息(过期的Shop shop = getById(id);//存在,先写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);//返回return shop;}public void saveShop2Redis(Long id, Long expireSeconds){//查询店铺数据Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));}
缓存工具封装
@Slf4j
@Component
public class CacheClient {// 注入redis 用构造函数注入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) {// 需要序列化成json字符串stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期// RedisData里面有时间和对象RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}// 缓存穿透 R为返回值类型的泛型 ID为id的类型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);}// 判断命中的是否是空值 不等于null,经过上面的筛选,就只剩下空的情况了if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.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) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.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);}
}
优惠券秒杀
为什么订单id不能自增长:id的规律性太明显,受单表数据量的限制(没有唯一性)
全局ID生成器
一种在分布式系统下用来生成全局唯一ID的工具:唯一性、高可用、高性能、递增性、安全性
为了增加安全性,不直接使用Redis自增的数值,而是拼接一些其他的信息:符号位1bit - 时间戳31bit - 序列号32bit(redis自增的值)
@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;}// keyPrefix为业务前缀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("yyyyMMdd"));// 2.2.自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); //如果只用keyPrefix,可能超过序列号32位的上限// 3.拼接并返回return timestamp << COUNT_BITS | count; // 时间戳左侧移动32位,再加上count的值}
}
添加秒杀券
/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀信息* @return 优惠券id*/@PostMapping("seckill")public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());}
@Override@Transactionalpublic 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中stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());}
实现秒杀券下单
两点需求:判断秒杀开始或者结束,库存是否充足
@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 查询SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}// 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {return Result.fail("库存不足");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderid = redisIdWorker.nextId("order");voucherOrder.setId(orderid);//用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 返回订单idreturn Result.ok(orderid);}
}
超卖问题
在线程一扣减之前,订单n开始进行查询
CAS法:compare and set
这里如果让stock和之前相等,就会有太多被过滤,成功率会太低
// 扣减库存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();
一人一单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;public Result seckillVoucher(Long voucherId) {// 查询SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}// 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 判断库存是否充足if (voucher.getStock() < 1) {return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();// id值一样的作为一把锁 intern() 只要字符串的值是一样的,那么就是一样的synchronized (userId.toString().intern()) {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象(事务)return proxy.createVoucherOrder(voucherId); // 先获取锁,在进入函数完成下单操作,函数执行完提交事务存入数据库后释放锁}}@Transactionalpublic 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("已经购买过了");}// 扣减库存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("库存不足");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderid = redisIdWorker.nextId("order");voucherOrder.setId(orderid);//用户idvoucherOrder.setUserId(userId);//代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 返回订单idreturn Result.ok(orderid);}
}
分布式锁
在集群模式下,会有多个JVM
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
// 创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:userId", stringRedisTemplate);// 获取锁boolean isLock = lock.tryLock(1200);// 判断是否获取锁成功if (!isLock) {// 失败,返回错误或重试return Result.fail("不允许重复下单");}try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象(事务)return proxy.createVoucherOrder(voucherId); // 先获取锁,在进入函数完成下单操作,函数执行完提交事务存入数据库后释放锁} finally {// 释放锁lock.unlock();}
分布式锁误删问题
在释放锁的时候判断锁的标识是否一致,别剪车锁把别人的给剪了
需求:获取锁时存入线程标识UUID,在释放锁时先获取标识
原子性问题
在释放锁没结束的时候阻塞,没释放完呢就阻塞了,之后超时释放锁,其他的线程拿到锁以后,这个阻塞结束了,这些自己把别人的锁给释放了
所以获取锁表示并判断是否一致和释放锁要有原子性。
public class SimpleRedisLock implements ILock {private String name; // 锁的key(名称),不同的业务有不同的锁,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);}@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁 key是拼接的串 value是线程的名称Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success); // 有自动拆箱会有空指针安全风险,所以进行比较}@Overridepublic void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());}/*@Overridepublic 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);}}*/
}
// 创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:userId", stringRedisTemplate);// 获取锁boolean isLock = lock.tryLock(1200);// 判断是否获取锁成功if (!isLock) {// 失败,返回错误或重试return Result.fail("不允许重复下单");}try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象(事务)return proxy.createVoucherOrder(voucherId); // 先获取锁,在进入函数完成下单操作,函数执行完提交事务存入数据库后释放锁} finally {// 释放锁lock.unlock();}
if(redis.call('get',KEYS[1]) == ARGV[1]) thenreturn redis.call('del',KEYS[1])
end
return 0
Redisson
<!-- redisson--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version></dependency>
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient RedissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://172.0.0.1").setPassword("123456");// 创建redisson对象return Redisson.create(config);}
}
// 创建锁对象//SimpleRedisLock lock = new SimpleRedisLock("order:userId", stringRedisTemplate);RLock lock = redissonClient.getLock("order:userId" + userId);// 获取锁boolean isLock = lock.tryLock(); //无参有默认值
Redisson可重入锁原理
Redisson的锁重试和watchdog机制
Redisson分布式锁原理:
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和PubSub功能实现等待、唤醒、获取锁失败的重试机制
超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
Redisson分布式锁主从一致问题
主节点处理写操作,从节点读操作,进行主从同步,如果有延时,那就会不一致,所以这里我们不要主从了,全变成独立的结点能读写,必须依次向所有的redis结点获取锁才能成功,如果有一个宕机,另外两个依旧有用(多主)
创建联锁
总结:
Redis秒杀优化
小姐姐接待顾客一条龙服务----小姐姐找后厨帮忙
判断秒杀库存用redis里的string,校验一人一单用set存id
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}//有元素线程被唤醒,没元素会被阻塞private BlockingDeque<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);private ExecutorService seckill_order_executor = Executors.newSingleThreadExecutor();@PostConstruct // 初始化完毕执行private void init(){seckill_order_executor.submit(new VoucherOrderHandler())}private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true) {// 获取队列中的订单信息try {VoucherOrder voucherOrder = orderTasks.take();//创建订单HandleVoucherOrder(voucherOrder);} catch (InterruptedException e) {log.info("处理订单异常",e);}}}}private void HandleVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();// 创建锁对象//SimpleRedisLock lock = new SimpleRedisLock("order:userId", stringRedisTemplate);RLock lock = redissonClient.getLock("order:userId" + userId);// 获取锁boolean isLock = lock.tryLock(); //无参有默认值// 判断是否获取锁成功if (!isLock) {// 失败,返回错误或重试return;}try {proxy.createVoucherOrder(voucherOrder);// 先获取锁,在进入函数完成下单操作,函数执行完提交事务存入数据库后释放锁} finally {// 释放锁lock.unlock();}}private IVoucherOrderService proxy;public Result seckillVoucher(Long voucherId) {//获取用户Long userId = UserHolder.getUser().getId();// 执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());// 判断结果是否为0int r = result.intValue();if(r!=0) {// 不为0 没有购买资格return Result.fail(r == 1 ? "库存不足":"不能重复下单");}// 为0 有购买资格 把下单信息保存到阻塞队列// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单idlong orderid = redisIdWorker.nextId("order");voucherOrder.setId(orderid);//用户idvoucherOrder.setUserId(userId);//代金券idvoucherOrder.setVoucherId(voucherId);// 放入阻塞队列orderTasks.add(voucherOrder);// 获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象(事务)//返回订单idreturn Result.ok(0);}@Transactional@Overridepublic void createVoucherOrder(VoucherOrder voucherOrder) {// 一人一单Long userId = UserHolder.getUser().getId();// 查询订单Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 判断是否存在if (count > 0) {return;}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1;.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {return;}// 创建订单save(voucherOrder);}
-- 参数列表--1.1 优惠券id
local voucherId = ARGV[1]-- 1.2 用户id
local userId = ARGV[2]-- 2 数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:' .. voucherId-- 3 脚本业务
-- 判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey) <= 0)) then-- 库存不足,返回1return 1
end
-- 判断用户是否下单
if(redis.call('sismenber', orderKey, userId) == 1) then-- 存在,重复下单return 2
end
-- 扣库存 incrby
-- 下单保存用户
redis.call('incrby', stockKey, -1)
redis.call('sadd',orderKey,userId)
return 0
Redis消息队列
Redis实现消息队列
list PubSub Stream
list
BRPUSH/BRPOP 加上B可以实现阻塞效果
优点:利用Redis存储,不受限于JVM存储上限;基于Redis的持久化机制,数据安全性有保证;可以满足消息有序性。
缺点:消息丢失,只支持单消费者
PubSub(发布订阅)
subscribe channel订阅频道
publish channel msg 向一个频道发送消息
psubscribe pattern
优点:支持多生产多消费
缺点:不支持数据持久化 无法避免消息丢失 消息堆积有上限,超出时数据丢失