利用setnx命令与lua脚本实现redis的分布式锁的误删与超卖问题

工具类中实现了get方法的通用接口,针对缓存穿透与缓存击穿问题做了处理。

并且利用setnx命令实现了分布式锁,将加锁与解锁的逻辑利用redis实现,避免了使用互斥锁时多台jvm所拥有的监视锁对象不一致的问题

package com.xiejianjun.tokenlogindemo.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.xiejianjun.tokenlogindemo.utils.RedisConstants.*;

/**
 * @author bilibilidick
 * @version 2022 06
 * @date 2022/6/26 13:07
 */
@Component
public class RedisUtils {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final DefaultRedisScript UNLOCKSCRIPT;

    static {
        UNLOCKSCRIPT = new DefaultRedisScript<>();
        UNLOCKSCRIPT.setLocation(new ClassPathResource("myredis.lua"));
    }

    public  E getWithMutex(Class clazz, String keyPrefix, ID id, long timeout, TimeUnit unit, Function function) {
        E value = null;
        try {
            // 从redis中查询相应ID的商品信息
            String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
            // 查到则直接返回
            if (StrUtil.isNotBlank(shopJson))
            {
                return JSONUtil.toBean(shopJson, clazz);
            }
            // 未查到则查询MYSQL数据库,查询到则将数据存储到redis中并返回,查不到则返回404
            // 为防止缓存穿透,如果未查到数据则缓存空字符串放入redis中
            if ("".equals(shopJson)) {
                return null;
            }
            // 如果缓存中不存在这个数据或者缓存已过期,则互斥的进行缓存重建
            if (tryLock(CACHE_SHOP_LOCK, id, "1")) {
                // 从redis中再次查询相应ID的商品信息
                shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
                // 查到则直接返回
                if (StrUtil.isNotBlank(shopJson))
                {
                    return JSONUtil.toBean(shopJson, clazz);
                }
                value = function.apply(id);
                if (value == null) {
                    // XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_EMPTY_TTL, TimeUnit.MINUTES);
                    set(keyPrefix + id, "",  CACHE_EMPTY_TTL, unit);
                    return null;
                } else {
                    // XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
                    set(keyPrefix + id, value,  timeout, unit);
                }
            } else {
                Thread.sleep(20);
                return getWithMutex(clazz, keyPrefix, id, timeout, unit, function);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unLock(CACHE_SHOP_LOCK, id);
        }

        return value;
    }

    public  void set(String key, E obj, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj), timeout, unit);
    }

    public boolean tryLock(String keyPrefix, E id, String value) {
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(keyPrefix + id, value, CACHE_LOCK_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(absent);
    }

    public void unLock(String keyPrefix, E id) {
        stringRedisTemplate.delete(keyPrefix + id);
    }

    public void orderLuaUnLock(List lockKey, Object... lockValue) {
        stringRedisTemplate.execute(UNLOCKSCRIPT, lockKey, lockValue);
    }

}

编写lua脚本保证释放锁的if判断语句与删除键两条命令的原子性,避免if判断语句之后jvm进入fullgc导致的stw进而使redis缓存过期导致的键值误删情况。

---
--- Generated by Luanalysis
--- Created by bilibilidick.
--- DateTime: 2022/6/28 17:51
---
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    return redis.call('del', KEYS[1])
end
return 0

实际使用时如果是单机系统,在利用jdk提供的互斥锁时,需要对互斥锁使用intern()方法,避免多个线程创建不同的对象。并且线程互斥代码块需套在事务注解外,避免释放锁后事务未提交的情况,且调用本类方法需利用代理对象调用,因为spring提供的事务注解自己实现的类中是没有的,需要转化为spring的代理类。

package com.xiejianjun.tokenlogindemo.service.impl;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sun.org.apache.xpath.internal.operations.Or;
import com.xiejianjun.tokenlogindemo.bean.Order;
import com.xiejianjun.tokenlogindemo.bean.Shop;
import com.xiejianjun.tokenlogindemo.bean.User;
import com.xiejianjun.tokenlogindemo.mapper.OrderMapper;
import com.xiejianjun.tokenlogindemo.service.OrderService;
import com.xiejianjun.tokenlogindemo.service.ShopService;
import com.xiejianjun.tokenlogindemo.utils.RedisIdWorker;
import com.xiejianjun.tokenlogindemo.utils.RedisUtils;
import com.xiejianjun.tokenlogindemo.utils.UserHolder;
import com.xiejianjun.tokenlogindemo.vo.Result;
import org.springframework.aop.framework.AopContext;
import org.springframework.aop.framework.AopProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import static com.xiejianjun.tokenlogindemo.utils.RedisConstants.CACHE_ORDER_LOCK;

/**
 * @author bilibilidick
 * @version 2022 06
 * @date 2022/6/28 12:55
 */
@Service
public class OrderServiceImpl extends ServiceImpl implements OrderService {

    @Autowired
    private ShopService shopService;

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;




    public Result secKillShopV1(long id) {
        synchronized (UserHolder.getUser().getId().toString().intern()) {
            OrderService proxy = (OrderService) AopContext.currentProxy();
            return proxy.syncUser(id);
        }
    }

    @Override
    public Result secKillShop(long id) {

            OrderService proxy;
            String lockValue = UUID.randomUUID(true).toString() + Thread.currentThread().getId();
            boolean result = redisUtils.tryLock(CACHE_ORDER_LOCK, UserHolder.getUser().getId().toString(), lockValue);
            if (result) {
                try {
                    proxy = (OrderService) AopContext.currentProxy();
                    return proxy.syncUser(id);
                } finally {
                    String nowLockValue = stringRedisTemplate.opsForValue().get(CACHE_ORDER_LOCK + UserHolder.getUser().getId().toString());
                    //if (lockValue.equals(nowLockValue)) {
                        // redisUtils.unLock(CACHE_ORDER_LOCK, UserHolder.getUser().getId().toString());
                    redisUtils.orderLuaUnLock(Collections.singletonList(CACHE_ORDER_LOCK + UserHolder.getUser().getId()), lockValue);
                    //}
                }
            } else {
                return Result.error("不要重复下单");
            }


    }

    @Transactional
    public Result syncUser(long id) {
        Shop targetShop = shopService.getById(id);
        if (targetShop == null) return Result.error("商品不存在");
        Integer stock = targetShop.getStock();
        if (stock > 0) {
            int count = count(new QueryWrapper().eq("userid", UserHolder.getUser().getId()));
            if (count > 0) return Result.error("不允许重复下单");
            int result = shopService.updateStockById(id);
            if (result != 0) save(new Order(redisIdWorker.nextTime("order"), UserHolder.getUser().getId(), targetShop.getName(), LocalDateTime.now()));
            return Result.ok("秒杀商品"+ targetShop.getName() +"成功");
        } else {
            return Result.error("商品库存不足");
        }
    }

}

你可能感兴趣的:(redis,分布式,lua,java,学习)