短信登录
将逻辑过期时间写入redis里面
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.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 {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sedCode(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.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
//5. 判断用户是否存在
if (user == null){
//6. 不存在,创建新用户
user = createUserWithPhone(phone);
}
//7.保存用户信息到session
session.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 {
@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();
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
表现层不变,业务层做一些改动,生成验证码存到redis里面,在登录校验的时候直接从redis里面读取验证码,再随机生成token,存入到redis里面
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public 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 120
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5. 发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
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);
//返回token
return 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 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public 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;
}
@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中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截
if (UserHolder.getUser()==null) {
//、没有 需要拦截
response.setStatus(401);
return false;
}
// 由用户,放行
return true;
}
}
商户查询缓存
缓存:数据交换的缓冲区(Cache),存储临时数据,一般读写性能高
缓存作用:降低后端负载
提高读写效率,降低响应时间
缓存成本:数据一致性成本
代码维护成本
运维成本
根据id查询商户信息:
@Override
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);
}
//不存在 根据id查询数据库
Shop shop = getById(id);
//数据库不存在,返回错误
if (shop == null) return Result.fail("店铺不存在");
//存在,先写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//返回
return Result.ok(shop);
}
}
给店铺类型查询业务添加缓存
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryTypeList() {
//先查询缓存 后面的名字是自己起的,是一个列表
String shopJson = stringRedisTemplate.opsForValue().get("cache:shopType:list");
//判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//存在直接返回 返回一个list
List<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("商铺类型不存在");
}
//数据库存在,先写入redis
stringRedisTemplate.opsForValue().set("cache:shopType:list",JSONUtil.toJsonStr(shopTypeList));
//返回
return Result.ok(shopTypeList);
}
}
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public 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);
}
}
@Transactional
public 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("店铺不存在");
}
//存在,先写入redis
stringRedisTemplate.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;
}
//存在,先写入redis
stringRedisTemplate.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));
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
@SpringBootTest
class HmDianPingApplicationTests {
@Autowired
private ShopServiceImpl shopService;
@Test
void 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);
//存在,先写入redis
stringRedisTemplate.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));
//写入redis
stringRedisTemplate.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)));
// 写入Redis
stringRedisTemplate.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) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
优惠券秒杀
为什么订单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
@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中
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
实现秒杀券下单
两点需求:判断秒杀开始或者结束,库存是否充足
@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
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("库存不足");
}
// 扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success) {
return Result.fail("库存不足");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单id
long orderid = redisIdWorker.nextId("order");
voucherOrder.setId(orderid);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 返回订单id
return 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 {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private 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); // 先获取锁,在进入函数完成下单操作,函数执行完提交事务存入数据库后释放锁
}
}
@Transactional
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("已经购买过了");
}
// 扣减库存
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();
// 订单id
long orderid = redisIdWorker.nextId("order");
voucherOrder.setId(orderid);
//用户id
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 返回订单id
return 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);
}
@Override
public 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); // 有自动拆箱会有空指针安全风险,所以进行比较
}
@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);
}
}*/
}
// 创建锁对象
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]) then
return 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 {
@Bean
public 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 {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private 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{
@Override
public 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()
);
// 判断结果是否为0
int r = result.intValue();
if(r!=0) {
// 不为0 没有购买资格
return Result.fail(r == 1 ? "库存不足":"不能重复下单");
}
// 为0 有购买资格 把下单信息保存到阻塞队列
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单id
long orderid = redisIdWorker.nextId("order");
voucherOrder.setId(orderid);
//用户id
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
// 放入阻塞队列
orderTasks.add(voucherOrder);
// 获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象(事务)
//返回订单id
return Result.ok(0);
}
@Transactional
@Override
public 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
-- 库存不足,返回1
return 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
优点:支持多生产多消费
缺点:不支持数据持久化 无法避免消息丢失 消息堆积有上限,超出时数据丢失