这里写目录标题
- 解决集群的session共享问题
-
- 使用rediss代替session共享登陆问题
- [修改保存验证码的方式为redis 【String类型】](#修改保存验证码的方式为redis 【String类型】)
- 登录的时候发送token到redis【Map类型】
- 把类型列表写入缓存中【List类型】
- 使用拦截器刷新用户有效期
- redis的缓存
- 缓存穿透
-
- [解决方案 添加空值返回](#解决方案 添加空值返回)
- 缓存雪崩
- 封装redis工具类
- redis实现优惠券秒杀
- 分布式锁
- redis的原子性
- 使用Redisson对分布式锁进行优化
-
- [Redisson 入门](#Redisson 入门)
- Rdisson可重入锁原理
- 详细教程
- 优化秒杀业务功能
- Redis的消息队列
- Redis实现点赞一人一赞功能
- Redis记录用户签到以及统计用户签到连续天数
解决集群的session共享问题
使用rediss代替session共享登陆问题
每次请求通过携带token到redis中判断是否存在该用户。
修改保存验证码的方式为redis 【String类型】
@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的key的有效期
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL ,TimeUnit.MINUTES);
//5. 发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回ok
return Result.ok();
}
登录的时候发送token到redis【Map类型】
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// TODO 2.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
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'
//7,1随机生产token作为登陆令牌
String token = UUID.randomUUID().toString().replace("-", "");
//7.2 将user对象转为hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fileName,fieldValue) -> fieldValue.toString()));
//7.3 使用has结构存储
String tokenkey =LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenkey,usermap);
//7.4给token设置有效期
stringRedisTemplate.expire(tokenkey,CACHE_SHOP_TTL,TimeUnit.MINUTES);
return Result.ok();
}
把类型列表写入缓存中【List类型】
@GetMapping("list")
public Result queryTypeList() {
//1.从redis查询分类缓存
List<String> shopTypeList = new ArrayList<>();
shopTypeList = stringRedisTemplate.opsForList().range(SHOP_TYPE,0,-1);
if (!shopTypeList.isEmpty()) {
List<ShopType> typeList = new ArrayList<>();
for (String str:shopTypeList) {
ShopType shopType = JSONUtil.toBean(str, ShopType.class);
typeList.add(shopType);
}
return Result.ok(typeList);
}
//2.不存在就查数据库
List<ShopType> typeList = typeService
.query().orderByAsc("sort").list();
if (typeList==null && typeList.size()>0){
return Result.fail("没有商品类型信息");
}
//存在添加redis缓存
for(ShopType shopType : typeList){
String s = JSONUtil.toJsonStr(shopType);
shopTypeList.add(s);
}
stringRedisTemplate.opsForList().rightPushAll(SHOP_TYPE,shopTypeList);
//3.转成json格式写入redis
return Result.ok(typeList);
}
使用拦截器刷新用户有效期
先刷新用户的token如果不存在就接着往下走,然后走到下一个拦截器中拦截。
package com.hmdp.utils;
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
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();
}
}
下一个拦截器来判断是否登陆了账号
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.是否需要去拦截
if (UserHolder.getUser()==null){
// 没有拦截设置状态吗
response.setStatus(401);
return false;
}
return true;
}
}
redis的缓存
添加缓存店铺查看缓存
package com.hmdp.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
//编写查询和缓存流程
@Override
public Result querybyId(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)){
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在直接返回错误
if (shop==null) {
return Result.fail("店铺吧存在");
}
//6.不存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
}
缓存更新策略
先操作数据库在写入缓存出现异常的情况较少。
先修改数据库在更新缓存
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (id==null){
return Result.fail("店铺id不能为空");
}
//1.更新数据库
updateById(shop);
//2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
缓存穿透
解决方案 添加空值返回
@Override
public Result querybyId(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)){
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if (shopJson==null) {
//缓存返回空
return Result.fail("店铺不");
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.不存在直接返回错误
if (shop==null) {
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺吧存在");
}
//6.不存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
缓存雪崩
缓存击穿
使用互斥锁解决缓存击穿问题
//解决缓存穿透
public Shop querybyIdWithMutex(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
if (shopJson != null) {
//缓存返回空
return null;
}
//4.1 获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
//4.实现缓存重建
boolean isLock = tryLock(lockKey);
//4.2 判断是否获取成功
if (!isLock) {
//4.3失败则休眠开始递归
Thread.sleep(50);
return querybyIdWithMutex(id);
}
//4.3.成功根据id查询数据库
shop = getById(id);
//5.不存在直接返回错误
if (shop == null) {
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.不存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//7 释放互斥锁
undelet(lockKey);
}
return shop;
}
添加redis线程锁
//添加锁
private boolean tryLock(String key) {
// 设置redis的分布锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
return BooleanUtil.isTrue(flag);
}
//删除锁
private void undelet(String key) {
stringRedisTemplate.delete(key);
}
逻辑过期方式
private static final ExecutorService CACHE_REDIIS=Executors.newFixedThreadPool(10);
//逻辑过期时间解决缓存穿透
public Shop querybyIdWithExpire(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return null;
}
//4.命中 需要把json反序列化对象
RedisData redisdata = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisdata.getData(), Shop.class);
LocalDateTime expireTime = redisdata.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期返回当前商户时间
return shop;
}
//5.2已过期缓存重建
//6缓存重建
//获取互斥锁
String lockKey = LOCK_SHOP_KEY+ id;
boolean isLock = tryLock(lockKey);
//6.2判断是否获取锁成功
if (isLock) {
//6.3成功开启独立线程
CACHE_REDIIS.submit(()->{
this.saveShop2Redis(id,20l);
//释放锁
undelet(lockKey);
});
}
return shop;
}
private void saveShop2Redis(Long id,Long expireSeconds) {
//1.查询店铺数据
Shop shop = getById(id);
//2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
}
封装redis工具类
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component
public class CacheClient {
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) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, 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)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
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);
}
// 判断命中的是否是空值
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);
}
}
redis实现优惠券秒杀
全局id生成器
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
实现优惠券下单效果
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
@Service
public class IVoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1,查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀结束");
}
//4.判断库存
if (voucher.getStock()<1){
//库存不足
return Result.fail("库存不足");
}
//5.扣减库存
boolean succes = seckillVoucherService.update().setSql("stock=stock - 1").eq("voucher_id", voucherId).update();
if (!succes){
return Result.fail("扣减失败");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单id
long orderId=redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userid = UserHolder.getUser().getId();
voucherOrder.setUserId( userid);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
解决优惠券超卖问题
在之前的写法高并发的场景下库存会出现超卖的问题。
乐观锁是用来解决更新数据时候的并发问题,
使用乐观锁解决超卖
使用CAS乐观锁解决
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
@Service
public class IVoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1,查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀结束");
}
//4.判断库存
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足");
}
//5.扣减库存
boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
.eq("voucher_id", voucherId)
//判断跟我查到到值跟数据库到值是否一致
.gt("stock", 0)
.update();
if (!succes) {
return Result.fail("扣减失败");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userid = UserHolder.getUser().getId();
voucherOrder.setUserId(userid);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
实现一人一单
//一人一单
Long user_id = UserHolder.getUser().getId();
//查询订单
Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();
//判断是否存在
if (count > 0) {
return Result.fail("购买过啦");
}
但是会有超卖问题
使用悲观锁解决一人一单超卖问题
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.User;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sun.awt.AppContext;
import javax.annotation.Resource;
import java.time.LocalDateTime;
@Service
public class IVoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1,查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀结束");
}
//4.判断库存
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足");
}
//5.扣减库存
boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
.eq("voucher_id", voucherId)
//判断跟我查到到值跟数据库到值是否一致
.gt("stock", 0)
.update();
if (!succes) {
return Result.fail("扣减失败");
}
//一人一单
Long user_id = UserHolder.getUser().getId();
//指定锁用户id,用户的id值一样的时候锁就一样,添加啦intern就不会每次都创建一个新的对象
//先获取锁
synchronized (user_id.toString().intern()) {
//如果不添加动态代理会导致当前事物不生效
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.creatVoutcherOrder(voucherId);
}
//等事物提交完在来释放锁
}
//添加悲观锁
@Transactional
public synchronized Result creatVoutcherOrder(Long voucherId) {
//一人一单
Long user_id = UserHolder.getUser().getId();
//指定锁用户id,用户的id值一样的时候锁就一样,添加啦intern就不会每次都创建一个新的对象
//查询订单
Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();
//判断是否存在
if (count > 0) {
return Result.fail("购买过啦");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户id
Long userid = UserHolder.getUser().getId();
voucherOrder.setUserId(userid);
//6.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
导入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
启动类添加动态代理
package com.hmdp;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
分布式锁
基于redis实现分布锁
package com.hmdp.utils;
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
分布式锁工具类
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
/*@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}*/
}
修改业务代码
public Result seckillVoucher(Long voucherId) {
//1,查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀结束");
}
//4.判断库存
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足");
}
//5.扣减库存
boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
.eq("voucher_id", voucherId)
//判断跟我查到到值跟数据库到值是否一致
.gt("stock", 0)
.update();
if (!succes) {
return Result.fail("扣减失败");
}
//一人一单
Long user_id = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + user_id, stringRedisTemplate);
//获取锁
boolean isLock = simpleRedisLock.tryLock(1200);
//判断是否获取锁成功
if (!isLock) {
//获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.creatVoutcherOrder(voucherId);
}finally {
simpleRedisLock.unlock();
}
//等事物提交完在来释放锁
}
极端情况下的误杀问题解决
业务阻塞后会出现极端情况,一个线程的锁超时释放后,这个进程结束阻塞的同时也会把另一个线程的锁给释放掉了。
解决方案,存放一个线程标识,是自己的锁才能释放不释放别人的锁。
/*@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);
}
}*/
redis的原子性
使用Redisson对分布式锁进行优化
Redisson 入门
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
创建redissonconfig配置文件
package com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RdissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://102.129.195.102:6379").setPassword("bxr0814");
//创建redissonclient对象
return Redisson.create(config);
}
}
接入redisson
省略之前代码
//一人一单
Long user_id = UserHolder.getUser().getId();
//创建锁对象 初始化redisson
RLock lock = redissonClient.getLock("lick:order:" + user_id);
//获取锁 无参表示失败立即返回
boolean isLock = lock.tryLock();
//判断是否获取锁成功
if (!isLock) {
//获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.creatVoutcherOrder(voucherId);
}finally {
lock.unlock();
}
Rdisson可重入锁原理
使用map结构来计算同一个线程进入了多少次锁。
详细教程
https://blog.csdn.net/daobuxinzi/article/details/137187008
优化秒杀业务功能
目前都是同步进行的,优化方案是使用异步。使用独立的线程来解决耗时的部分。
新增优惠券的同时将优惠券信息保存到redis中
// 保存秒杀库存到Redis中
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
基于lua脚本判断秒杀库存一人一单。
开启线程任务实现异步下单
// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl
/** 异步执行下单动作的线程池 */
private static final 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循环持续读取队列中的信息
while (true) {
try {
log.info("=====begin=====>");
// 1.获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
log.info("get from queue : {}", voucherOrder.toString());
// 2.创建订单
handleVoucherOrder(voucherOrder);
log.info("=====end=====>");
} catch (Exception e) {
log.error("处理异常订单", e);
}
}
}
/**
* 处理订单
*/
private void handleVoucherOrder(VoucherOrder voucherOrder) {
// 1.创建锁对象
RLock rLock = redissonClient.getLock("lock:order:" + voucherOrder.getUserId());
// 2.尝试获取锁
boolean isLock = rLock.tryLock();
log.info("isLock = {}", isLock);
// 3.判断是否获取锁成功
if(!isLock) {
// 获取锁失败
log.error("不允许重复下单!");
return;
}
try {
// 4.持锁真正创建订单
checkAndCreateVoucherOrder(voucherOrder.getVoucherId(), voucherOrder.getUserId());
} finally {
// 5.释放锁
rLock.unlock();
log.info("unlock done.");
}
}
/**
* 持锁真正创建订单
*/
private void createVoucherOrder(VoucherOrder voucherOrder) {
log.info("begin createVoucherOrder... voucherId = {}, userId = {}, orderId = {}",
voucherOrder.getVoucherId(), voucherOrder.getUserId(), voucherOrder.getId());
// 1.增加一人一单规则
int count = query().eq("voucher_id", voucherOrder.getVoucherId())
.eq("user_id", voucherOrder.getUserId()).count();
log.info("old order count = {}", count);
if(count > 0) {
// 该用户已下过单
log.error("每个帐号只能抢购一张优惠券!");
return;
}
// 2.扣减库存
boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock", 0)
.update();
log.info("update result = {}", update);
if(!update) {
// 扣减库存失败,返回抢券失败
log.error("库存不足,抢券失败!");
return;
}
// 3.创建订单
voucherOrder.setPayTime(new Date());
voucherOrderService.save(voucherOrder);
}
}
Redis的消息队列
Redis实现点赞一人一赞功能
//点赞接口
@Override
public Result likeBlog(Long id) {
//1.获取登陆用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登陆用户是否已经点赞
String key = "blog:liked:"+ id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if (BooleanUtil.isFalse(isMember)) {
//3.如果未点赞,可以点赞
//3.1 数据库点赞+1
boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();
//3.2保存用户刀redis的set集合
if (isSuccess){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
}else {
//4.如果已点赞 取消点赞
//4.1 数据库点赞-1
boolean isSuccess = update().setSql("liked=liked-1").eq("id", id).update();
stringRedisTemplate.opsForSet().remove(key,userId.toString());
//3.2保存用户刀redis的set集合
if (isSuccess){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
}
return Result.ok();
}
//判断是否点赞
private void isBlogLiked(Blog blog) {
//1.获取登陆用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登陆用户是否已经点赞
String key = "blog:liked:"+ blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
boolean aTrue = BooleanUtil.isTrue(isMember);
blog.setIsLink(aTrue);
}
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog ->{
//查询用户信息
this.queryBlogUser(blog);
//判断是否点赞
this.isBlogLiked(blog);
});
return Result.ok(records);
}
Redis记录用户签到以及统计用户签到连续天数
@Override
public Result sign() {
//1.获取当前登陆用户
Long userId = 1010l;
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = "user_sing_" + userId + keySuffix;
//4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
//5.写入redis setbit key offset 1
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
@Override
public Result signCount() {
// 获取当前登录用户
Long userId = 1010l;
// 获取当前日期
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
// 拼接 key
String key = "user_sing_" + userId + keySuffix;
// 获取今天是本月的第几天
// 当前日期为 2022年5月12号,故而 dayOfMonth = 12
int dayOfMonth = now.getDayOfMonth();
// 获取从 0 到 dayOfMonth 的签到结果,返回的是个十进制的数字
List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
//redis的子命令,从当前日期开始到0开始查
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if(result == null || result.isEmpty()){
// 没有签到结果
return Result.ok(0);
}
Long num = result.get(0);
if (num == null || num == 0) {
return Result.ok(0);
}
int count = 0;
// 循环遍历
while(true){
// 让这个数字与 1 做与运算,得到数字的最后一位 bit 位 判断这个 bit 位是否为0
// num & 1 做与运算,其中 1 的左边以 0 补齐
if ((num & 1) == 0) {
// 如果为 0,说明未签到,结束
break;
} else {
// 如果不为 0,说明已签到,计数器 +1
count++;
}
// 把数字右移一位,抛弃最后一个 bit 位,继续下一个 bit 位
// >> :右移 最高位是0,左边补齐0;最高为是1,左边补齐1
// << :左移 左边最高位丢弃,右边补齐0
// >>>:无符号右移 无论最高位是0还是1,左边补齐0
// num >>>= 1 ------------> num = num >>> 1
num >>>= 1;
}
return Result.ok(count);
}