session共享问题:多台服务器并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
主要逻辑:验证手机号格式 如果不符合,返回错误信息,如果符合生成验证码,就运用redis的String数据结构对手机号和验证码进行存储 redis存储一般有个公共前缀并且设置有效时间 一般两分钟 最后返回ok;
//Slfg4 日志注解
public Result sendCode(String phone, HttpSession session) {
//1:先验证手机号格式 不符合就返回错误信息
if(RegexUtils.isPhoneInvalid(phone)){
//2:如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//4:生成验证码
String code = RandomUtil.randomNumbers(6);
//保存验证码到redis phone作为key 并且有一个公共前缀
stringRedisTemplate.opsForValue()
.set(RedisConstants.LOGIN_CODE_KEY +phone,code
,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
//6:发送验证码
log.debug("验证码是{}",code);
return Result.ok();
}
主要逻辑:前端给手机号,以手机号从redis里面拿出验证码,比较验证码,如果不等则返回错误"验证码不正确",如果相等看看是否被注册过,如果没有被注册过,需要插入数据,没注册过的话直接返回UserDto对象(注:UserDto对象相比User对象少了一些敏感信息,例如:密码),然后将对象存储到redis的hash结构里面,并设置有效期 一般为30分钟
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//获取手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//2:如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//从reids中拿出验证码
String code = stringRedisTemplate.opsForValue()
.get(RedisConstants.LOGIN_CODE_KEY + phone);
if(loginForm.getPhone().equals(code)){
//如果验证码错误直接返回false
return Result.fail("验证码不正确");
}
//如果正确 在确定手机号是否已经被注册过
User user = query().eq("phone", phone).one();
//生成token 用hutool工具类生成的uuid toString(true)可以把uuid中的-去掉
String token = UUID.randomUUID().toString(true);
if(user==null){
//没有注册过新建并插入新数据
user=CreateNewUser(phone);
}
//hutool工具类 Beanutil
UserDTO userDTO= BeanUtil.copyProperties(user, UserDTO.class);
//运用redis中的map数据结构存储userDto对象
Map<String,String> map=new HashMap<>();
map.put("id",userDTO.getId().toString());
map.put("nickName",userDTO.getNickName());
map.put("icon",userDTO.getIcon());
stringRedisTemplate.opsForHash()
.putAll(RedisConstants.LOGIN_USER_KEY+token,map);
//设置时间一般是30分钟不进行操作,就会失效
stringRedisTemplate
.expire(RedisConstants.LOGIN_USER_KEY+token,
RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
private User CreateNewUser(String phone) {
User user=new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(4));
save(user);
return user;
}
主要逻辑:一个拦截器是更新拦截器,主要是更新token的有效期,一个拦截不合法的路径,更新拦截器首先获得token对象 如果token为空直接放行。若不为空的话token刷新token的有效期,然后用token从redis里面拿出UserDTO的map对象,然后把map对象转换为UserDTO对象,存入ThreadLocal域中。在拦截器执行之后将TheadLocal域中的对象释放掉,避免发生内存泄漏.一个拦截器只用判断ThreadLocal域中有没有UserDTO对象,如果有则放行,如果没有就拦截.
//更新拦截器 主要是更新token有效期 另外拦截器不是spring管理的bean
//里面不能用自动注入注解 需要用构造方法
public class RefreshInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//从对象头获得token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
return true;
}
//若不为空放行,并且把用户放进TheadLocal并且把时间重置为30分钟
Map<Object, Object> map = stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY+token);
if(map.isEmpty()){
return true;
}
//hutool工具类 将map转换为实体类对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
UserHolder.saveUser(userDTO);
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,
RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//释放Thread中的user类 避免内存泄露
UserHolder.removeUser();
}
}
//目的是拦截不合法的路径
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request
, HttpServletResponse response, Object handler) throws Exception {
System.out.println("执行拦截器");
System.out.println(UserHolder.getUser());
if(UserHolder.getUser()==null){
//状态码401 表示没授权
response.setStatus(401);
return false;
}
return true;
}
}
拦截器的配置
@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"
).order(1);
//刷新放行拦截器
registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}
缓存就是数据交换的缓冲区,是存储数据的临时地方,一般读写性能比较高
缓存的作用?
缓存的成本?
具体流程:根据前端返回的id的数据,查商品信息,接收到id后,先从redis里面拿数据,如果有直接返回,如果没有在从数据库里面拿商品信息,如果没有报错商品不存在,如果有先往redis缓存里存入数据并且设置有效期(避免缓存数据与数据库的数据长期不一致)。
@Override
public Result queryShopById(Long id) {
//根据id看看redis有没有缓存
String shopString = stringRedisTemplate.opsForValue()
.get(RedisConstants.CACHE_SHOP_KEY + id);
if(StrUtil.isNotBlank(shopString)){
//如果有 直接返回
//hutool工具类
Shop shop = JSONUtil.toBean(shopString, Shop.class);
return Result.ok(shop);
}
//如果没有从数据库查
Shop shop = getById(id);
if(shop==null){
//数据库没有 返回404 没有该商品
return Result.fail("该店铺不存在");
}
//数据库有 往redis插入数据 并且返回数据
stringRedisTemplate.opsForValue()
.set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil
.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
操作缓存和数据库有三个问题?
删除缓存还是更新缓存
如何保证缓存与数据库的操作同时成功或失效
先操作缓存还是先操作数据库?
具体流程:首先获得前端给的修改的数据 ,判断id是否为空,为空直接返回错误,不为空就先更新数据库,在删除redis的缓存
@Override
@Transactional//事务注解
public Result updateShop(Shop shop) {
Long id = shop.getId();
if(id==null){
//先检查id是否为空
return Result.fail("商品id不能为空");
}
//先更新数据库
updateById(shop);
//在删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+id);
return Result.ok();
}
具体流程 如果没有该商品 则往redis插入一个空字符串,设置短的有效期,下一次如果在redis里面查的是空字符串的话,则直接返回商品不存在
//缓存穿透解决方案
if(StrUtil.isNotBlank(shopString)){
//如果有 直接返回
Shop shop = JSONUtil.toBean(shopString, Shop.class);
return Result.ok(shop);
}
//判断字符串不为null 则为一个空字符串 直接返回404 不用经过数据库查询
if("".equals(shopString)){
return Result.fail("该商铺不存在");
}
if(shop==null){
//数据库没有 则往redis插入一个空字符串,并且设置一个短的有效期
stringRedisTemplate.opsForValue()
.set(RedisConstants.CACHE_SHOP_KEY+id,""
,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("该店铺不存在");
}
缓存击穿问题也叫做热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数请求在瞬间给数据库带来巨大的冲击
常见的解决方案有两种:
互斥锁解决
具体流程:当redis里面查不到之后,先上锁,锁用的是redis的String数据结构,如果上锁失败,先睡眠,在重新去获得数据.如果上锁成功,进行第二次从redis里面查,如果还查不到,从数据库查,并且插入reids数据,释放锁,返回数据.
private Shop queryWithMutex(Long id){
//获得key
String key=RedisConstants.CACHE_SHOP_KEY + id;
//从redis里面获得数据
String shopString = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(shopString)){
Shop shop = JSONUtil.toBean(shopString, Shop.class);
return shop;
}
if("".equals(shopString)){
return null;
}
//获得锁的key
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
Shop shop=null;
try {
//尝试获得锁
if (!tryLock(lockKey)) {
//如果没有获的 睡眠 50ms 重新获取值
Thread.sleep(RedisConstants.LOCK_SLEEP_TTL);
return queryWithMutex(id);
}
//如果获得锁 进行第二次 从redis里面获得数据
String string = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(string)) {
shop = JSONUtil.toBean(string, Shop.class);
return shop;
}
if ("".equals(string)) {
return null;
}
//第二次没获得 从数据库查
shop = getById(id);
//没查到赋空字符串
if (shop == null) {
stringRedisTemplate.opsForValue()
.set(RedisConstants.CACHE_SHOP_KEY + id, "",
RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//查到则直接往redis里面插入值
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
}catch (InterruptedException e){
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
return shop;
}
逻辑过期(感觉挺重要)
具体流程:一般这种热点key在使用之前需要进行预热,也就是把数据先提前送到缓存中,并设置一个逻辑时间,然后拿到id查数据,如何缓存里面没有则直接返回null,如果有则查看它的逻辑过期时间是否已经过期,如果过期则拿锁,若拿不到直接返回旧值,如果拿到了再从redis拿出来看看是否过期,如果这次没过期,则直接返回,如果依然过期,则开启一个新的线程将商铺数据重新写入redis,最后释放互斥锁
//首先设置一个Shop和过期时间的Bean
@Data
//@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
//自定义一个线程用于开启线程
private static final ExecutorService CACHE_REBUILD_EXECUTOR=
new ThreadPoolExecutor(10,10,0,
TimeUnit.MINUTES,new LinkedBlockingDeque<>());
private Shop queryWithLogicalExpire(Long id){
//从redis拿数据
String shopString = stringRedisTemplate.opsForValue()
.get(RedisConstants.CACHE_SHOP_KEY + id); //如果为空 直接返回空
if(StrUtil.isBlank(shopString)){
return null;
}
//不为空拿出对象和过期时间
RedisData redisData = JSONUtil.toBean(shopString, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//如果过期时间没到期 直接返回对象
if(redisData.getExpireTime().isAfter(LocalDateTime.now())){
return shop;
}
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
//如果过期时间到了 尝试获取锁
Boolean lock = tryLock(lockKey);
if(!lock){
//如果没拿到锁直接返回旧数据
return shop;
}
//拿到锁后在进行一次验证过期时间如果这一次已经被修改了 则直接返回
shopString = stringRedisTemplate.opsForValue()
.get(RedisConstants.CACHE_SHOP_KEY + id);
if(StrUtil.isNotBlank(shopString)){
redisData = JSONUtil.toBean(shopString, RedisData.class);
shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
if(redisData.getExpireTime().isAfter(LocalDateTime.now())){
return shop;
}
}
//如果没被修改 运用自定义线程池 开启一个新的线程 进行更新缓存操作
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
this.saveRedis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}
finally {
this.unLock(lockKey);
}
});
return shop;
}
//逻辑过期 缓存预热
public void saveRedis(Long id,Long outTime){
Shop shop = getById(id);
RedisData rs=new RedisData();
rs.setData(shop);
rs.setExpireTime(LocalDateTime.now().plusSeconds(o3utTime));
stringRedisTemplate.opsForValue()
.set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(rs));
}
全局id生成器,一般需要满足一下几个特征:
id的组成部分:
@Component
public class RedisIdWorker {
//开始的时间戳 用的是2022/10/19 的时间戳
private static final long BEGIN_TIMESTAMP=1666137600L;
//序列号长度
private static final int COUNT_BITS=32;
@Resource
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix){
//生成时间戳
LocalDateTime now = LocalDateTime.now //2022-10-19T16:07:19.784197600
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
Long timestamp=nowSecond-BEGIN_TIMESTAMP;
//生成序列号
//获得当前日期,精确到天
//key中有年月日,方便计算一年一月一天的销售总量
String date=now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
//自增长
Long count=stringRedisTemplate.opsForValue()
.increment("inc"+keyPrefix+":"+date);
//将生成的时间戳向右移动32位 然后将序列号或到后32位
return timestamp<<COUNT_BITS|count;
}
}
@Resource
private RedisIdWorker redisIdWorker;
//线程池 注意线程池里面的线程必须比CountDownLatch中定义的值多
private ExecutorService ex=new ThreadPoolExecutor(500,500,0,
TimeUnit.SECONDS,new LinkedBlockingDeque<>());
@Test
void m1() throws InterruptedException {
//CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕
CountDownLatch latch=new CountDownLatch(300);
Runnable task=()-> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id=" + id);
}
//执行一次用countDown减一
latch.countDown();
};
long begin=System.currentTimeMillis();
for(int i=0;i<300;i++){
ex.submit(task);
}
//全部执行完 才能往下执行
latch.await();
Long end=System.currentTimeMillis();
System.out.println("time="+(end-begin));
}
public Result seckillVoucher(Long voucherId) {
//获得秒杀优惠卷的id
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if(voucher==null){
return Result.fail("优惠卷不存在");
}
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();
//把userId当成锁 toString() 会将id变成字符串
// 但是toString的源码只是new 了一个新字符串
//同个id toString()还是不同对象 所以用intern()方法 将字符串放入字符串常量池 并返回
synchronized (userId.toString().intern()) {
//获取代理对象(事务) 处理事务失效问题 还没学到 以后解决
IVoucherOrderService proxy=
(IVoucherOrderService)AopContext.currentProxy();
/*这个对象需要在接口中声明
并且在启动类加入@EnableAspectJAutoProxy(exposeProxy = true)注解
暴露代理对象 还需要加入aspectjweaver 依赖
*/
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//为什么不在方法里面加锁?
//因为 在释放锁之后事务spring才提交事务 释放锁
//还没提交的时候 可能另一个线程可能拿到锁
//线程不安全
Long userId= UserHolder.getUser().getId();
//实现一人一单
int count = query().eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if(count>0){
return Result.fail("您已经购买到了");
}
//前面已经判断库存是否充足
//这次在判断 是一种乐观锁的机制
//运用cas机制 在进行库存加减的时候 需要再次进行判断 库存是否有
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId)
//
.gt("stock",0)
.update();
if(!success){
return Result.fail("优惠卷已经被发放完");
}
VoucherOrder voucherOrder=new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
this.save(voucherOrder);
return Result.ok(orderId);
}
然而上述锁只能运用在单体项目中,如果在分布式项目上并不能起到一人一单功能,所以需要分布式锁
分布式锁:满足分布式系统或集群模式下多线程可见并且互斥的锁
private StringRedisTemplate stringRedisTemplate;
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
@Override
// 上锁
public boolean tryLock(long timeoutSec) {
//运用uuid和线程id生成 value
String threadId = ID_PREFIX+Thread.currentThread().getId();
//前缀和name组成 key
//设置过期时间 如果redis宕机后 锁还能等时间结束后释放 避免造成死锁
Boolean success = stringRedisTemplate
.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId
,timeoutSec, TimeUnit.SECONDS);
return BooleanUtil.isTrue(success);
}
@Override
public void unLock() {
//在释放锁的时候 如果线程一 拿到锁 但是进行阻塞 然后锁失效了
//线程二拿到锁 线程一在阻塞消失后 直接删除了线程二的锁
//解决方案 lua脚本
String threadID = ID_PREFIX+Thread.currentThread().getId();
String id1=stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadID.equals(id1)){
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
--- lua脚本能保证代码执行的原子性
if(redis.call('get',KEYS[1])==ARGV[1]) then
return redis.call('del',KEYS[1])
end
return 0
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
//初始化
UNLOCK_SCRIPT=new DefaultRedisScript<>();
//设置脚本位置 classPath下的资源
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
//设置返回值
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unLock() {
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+name),
ID_PREFIX+Thread.currentThread().getId()
);
}
使用Redisson分布式锁
Redisson可重入锁的原理
Redisson的锁重试,和看门狗机制
异步秒杀思路
异步秒杀的主要流程:在秒杀的时候 判断库存是否充足 如果不充足 直接返回错误,如果是充足的话,将优惠卷id,用户id和订单id存入阻塞队列,另开线程进行数据库交互
-- lua脚本在判断库存是否充足时 是原子性的 避免产生线程问题
-- 优惠卷id
local voucherId=ARGV[1]
-- 1.2 用户id
local userId=ARGV[2]
-- 2.数据key
-- 2.1 库存key
-- .. 字符串连接符
local stockKey='seckill:stock:' .. voucherId
local orderKey='seckill:order:' .. voucherId
--3脚本业务
--3.1判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey))<=0) then
return 1
end
if(redis.call('sismember',orderKey,userId)==1) then
return 2
end
--3.4 扣库存
redis.call('incrby',stockKey,-1)
redis.call('sadd',orderKey,userId)
return 0
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements IVoucherOrderService {
@Resource
private SeckillVoucherServiceImpl seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
private IVoucherOrderService proxy;
/*阻塞队列特点:当一个线程尝试从队列中获取元素,
没有元素,线程就会被阻塞,直到队列中有元素,线程才会被唤醒,并去获取元素
*/
private BlockingQueue<VoucherOrder> orderTasks
=new ArrayBlockingQueue<>(1024*1024);
//线程池的创建
private static final ExecutorService SECKILL_ORDER_EXECUTOR=
Executors.newSingleThreadExecutor();
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
//初始化
SECKILL_SCRIPT=new DefaultRedisScript<>();
//设置脚本位置 classPath下的资源
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
//设置返回值
SECKILL_SCRIPT.setResultType(Long.class);
}
//spring的知识 目的是为了让 在类创建时 对这个方法进行初始话
@PostConstruct//spring注解
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
@Override
public Result seckillVoucher(Long voucherId){
//执行lua脚本
Long userId = UserHolder.getUser().getId();
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
int r=result.intValue();
//判断结果为0
if(r!=0){
return Result.fail(r==1?"库存不足":"不能重复下单");
}
// 保存阻塞队列 将新建的对象存入阻塞队列
VoucherOrder voucherOrder=new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
//不为0 代表没有购买职责
//处理spring事务失效问题
proxy=(IVoucherOrderService) AopContext.currentProxy();
//加入队列 开辟线程
orderTasks.add(voucherOrder);
//直接返回
return Result.ok(orderId);
}
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true){
try{//这个线程主要从阻塞队列拿出voucherOrder对象 一直循环
VoucherOrder voucherOrder=orderTasks.take();
handleVoucherOrder(voucherOrder);
}catch (Exception e){
log.error("处理订单异常",e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder){
//不能在线程里面拿对象了 因为线程变了
Long userId=voucherOrder.getUserId();
//分布式锁 获取锁 双重保障
RLock lock = redissonClient.getLock("lock:order"+userId);
boolean b = lock.tryLock();
if(!b){
log.error("不允许重复下单");
return;
}
try{
//调用方法
proxy.createVoucherOrder(voucherOrder);
}finally {
lock.unlock();
}
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId= voucherOrder.getUserId();
//实现一人一单
int count = query().eq("user_id", userId)
.eq("voucher_id",voucherOrder.getVoucherId()).count();
if(count>0){
log.error("用户已经购买过一次!");
return;
}
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock",0)
.update();
if(!success){
log.error("库存不足");
}
this.save(voucherOrder);
}
}
需求:
实现步骤:
@Override
public Result likeBlog(Long id) {
//获得登录信息
Long userId = UserHolder.getUser().getId();
//判断当前登录用户是否已经点赞
//往redis里面存入key是前缀加上博客id,value是用户id
Boolean member = stringRedisTemplate.opsForSet()
.isMember(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
if(BooleanUtil.isFalse(member)){
//如果未点赞,可以点赞
//数据库点赞+1
boolean success = update().setSql("liked=liked+1")
.eq("id", id).update();
if(success) {
//保存用户到redis的set集合
//stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id,
// userId.toString(),System.currentTimeMillis());存放在zet中
stringRedisTemplate.opsForSet()
.add(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
}
}else{
//如果已点赞,取消点赞
//数据库点赞数-1
boolean success = update().setSql("liked=liked-1").eq("id", id).update();
if(success) {
//把用户从redis的set集合移除
stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
}
}
return Result.ok();
}
//在每次查询的时候需要判断该用户是否已经点赞了这个博客
private void isBlogLiked(Blog blog){
// 获得这个用户
Long userId = UserHolder.getUser().getId();
//key前缀加博客id
String key=RedisConstants.BLOG_LIKED_KEY+blog.getId();
//查redis里面有没有数据 如果有 isLike返回假 如果没有 则返回真
Boolean isMember = stringRedisTemplate.opsForSet()
.isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
在实现点赞排行榜时 不能用set集合做判断了 因为set是无序的 因此将set要改成zset 将时间戳存放到score中 实现排行
@Override
public Result queryBlogLikes(Long id) {
//1 查询top5的点赞用户
String key=RedisConstants.BLOG_LIKED_KEY+id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
//解析出其中的用户id
if(top5==null||top5.isEmpty()){
return Result.ok(Collections.emptyList());
}
//运用了jdk8中的新特性 有时间学学 将set集合中的String统统改为Long
List<Long> ids = top5.stream().map(Long::valueOf)
.collect(Collectors.toList());
//hutool下的string工具类
String idsStr = StrUtil.join(",", ids);
//用in不会根据根据自己的顺序进行排序
//需要用 select * from tb_user where in(5,1) order by field(id,5,1)
List<UserDTO> userDTOS = userService.query()
.in("id",ids)
.last("ORDER BY FIELD(id,"+idsStr+")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//获得登录用户
Long userId = UserHolder.getUser().getId();
//判断到底是关注还是取关
if(isFollow){
//直接往表里插入数据
Follow follow=new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);
}else {
//删除数据
remove(new QueryWrapper<Follow>()
.eq("user_id", userId).eq("follow_user_id", followUserId));
}
//关注,新增取关
//取关,删除
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
//查看有没有关注
Long userId = UserHolder.getUser().getId();
Integer count = query()
.eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count>0);
}
在开发共同关注时,需要将关注和取关功能的功能改善一下 需要将用户id的key和关注的用户的id作为value 放到set 因为set可以进行查找供同拥有的value
@Override
public Result followCommons(Long id) {
//用户id
Long userId=UserHolder.getUser().getId();
//用户id key
String key=RedisConstants.FOLLOW+userId;
//查找另一个用户的id
String key1=RedisConstants.FOLLOW+id;
//进行value查重
Set<String> intersect = stringRedisTemplate
.opsForSet().intersect(key, key1);
if(intersect==null||intersect.isEmpty()){
return Result.ok(Collections.emptyList());
}
List<Long> ids= intersect.stream().map(Long::valueOf)
.collect(Collectors.toList());
List<UserDTO> collect = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(collect);
}
feed流
拉模式:大v都有自己的发件箱,当发消息的时候会发到自己的发件箱里面,等用户上线查看收件箱,会将用户关注的所有大v的发件箱复制一份放到用户的收件箱,重新按时间戳进行排序,供用户读取!弊端:如果这个人是个变态,关注着几千多个人,成千上万个数据会复制到收件箱,耗费内存
推模式:大v在写消息时,会将关注自己的所有人的收件箱里面写一份,供粉丝阅读。缺点:如果大v有太多粉丝,也会造成太耗费内存
拉推结合模式:对待僵尸粉采用拉模式,对待活跃粉丝采用推模式
本次实现使用的是推模式
//在发布笔记时,向各个用户的收件箱发送 运用的时有序zset集合
@Override
public Result saveBlog(Blog blog) {
//获得登录用户/
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
boolean success = save(blog);
if(!success){
return Result.fail("新增笔记失败");
}
//查询该用户的所有粉丝
select * from tb_follow where follow_id ='user.getId()'
List<Follow> follows = followService.query()
.eq("follow_user_id", user.getId()).list();
//
long l = System.currentTimeMillis();
for(Follow follow:follows){
//对每个粉丝的收件箱进行推送
String key=RedisConstants.FEED_KEY+follow.getUserId();
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),l);
}
return Result.ok(blog.getId());
}
//滚动查询的意思是 当查询的时候 本来是10条数据 分页 一页两条数据 如果这时候插入一条新数据 索引变化 分页会重复查询 数据 这时候需要滚动查询 在查询第二页数据时 记录最后一个数据的score数据 并记录 这一页score 有几个 然后根据这个score数据查第三页
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//获得当前用户id
Long userId = UserHolder.getUser().getId();
//查询收件箱 key max最大值 min最小值 limit offset偏移量 count 分页数量
String key=RedisConstants.FEED_KEY+userId;
//value 是blogId score 就是分数
Set<ZSetOperations.TypedTuple<String>> typedTuples =
stringRedisTemplate.opsForZSet().
reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//非空判断
if(typedTuples==null||typedTuples.isEmpty()){
return Result.ok();
}
//解析数据:blogId,minTime,offset
ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
long minTime=0;
int os=1;
for(ZSetOperations.TypedTuple<String> tuple:typedTuples){
ids.add(Long.valueOf(tuple.getValue()));
long time =tuple.getScore().longValue();
if(time==minTime){
os++;
}else{
minTime=time;
os=1;
}
}
String idStr=StrUtil.join(",",ids);
List<Blog> blogs = query().in("id", ids)
.last("ORDER BY FIELD(id," + idStr + ")").list();
for(Blog blog:blogs){
//查询blog有关的用户
queryBlogUser(blog);
//查询blog是否被点赞
isBlogLiked(blog);
}
ScrollResult r=new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}
@Override
public Result sign() {
//获取当前用户
Long userId = UserHolder.getUser().getId();
//获取日期
LocalDateTime now=LocalDateTime.now();
String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
//拼接key
String key=RedisConstants.USER_SIGN_KEY+userId+keySuffix;
//获得今天是本月第几天
int dayOfMonth = now.getDayOfMonth();
//写入redis
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
@Override
public Result signCount() {
//获取当前用户
Long userId = UserHolder.getUser().getId();
//获取日期
LocalDateTime now=LocalDateTime.now();
String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
//拼接key
String key=RedisConstants.USER_SIGN_KEY+userId+keySuffix;
//获得今天是本月第几天
int dayOfMonth = now.getDayOfMonth();
List<Long> result=stringRedisTemplate.opsForValue().bitField(key,
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType
.unsigned(dayOfMonth)).valueAt(0));
if(result==null||result.isEmpty()){
//没有任何签到结果
return Result.ok(0);
}
Long num=result.get(0);
if(num==null || num==0){
return Result.ok(0);
}
//循环遍历
int count=0;
while(true){
//让这个数字与1做与运算,得到数字的最后的一个bit
if((num&1)==0){
//如果为0,说明未签到 ,结束
break;
}else{
//如果不为0,说明已签到,计数器+1
count++;
}
//把数字右移一位,抛给最后的比特位,继续下一个bit位
num>>>=1;
}
return Result.ok(count);
}
jdk8的新特性学一下 spring再重新学一下