编码技巧——Lua脚本的应用及库存扣减场景应用

demo包括lua脚本文件、文件读入、redis命令执行脚本;

(1)Lua脚本

位置放在resource目录下:

编码技巧——Lua脚本的应用及库存扣减场景应用_第1张图片

脚本较多,列举几个:

1. 如果key存在,自减返回计算后的值

local key = KEYS[1]
local usedstore = tonumber(redis.call('get', key))
if usedstore ~=nil and usedstore>0 then
    local current = tonumber(redis.call('decr', key))
    return current
end
return usedstore

2. 先执行hset,成功后设置过期时间,返回命令结果

local key = KEYS[1]
local field, duration, value =ARGV[1], tonumber(ARGV[2]), ARGV[3]
local current = tonumber(redis.call('hset', key, field, value))
if current == 1 then
    redis.call('expire', key, duration)
end
return current

3. 先执行set,再执行expire

local key = KEYS[1]
local duration, value = tonumber(ARGV[1]), ARGV[2]
local current = redis.call('set', key, value)
if current ~= nil then
    redis.call('expire', key, duration)
    return 1
end
return 0

4. 库存扣减,当前库存>0则执行扣减,返回扣减后库存;如果已经为0则直接返回-1标识失败

local key = KEYS[1]
local usedstore = tonumber(redis.call('get', key))
if usedstore ~= nil and usedstore > 0 then
    local current = tonumber(redis.call('decr', key))
    return current
end
--若此时库存为0,则直接返回-1,不写redis;在库存不足时减小写压力(1次扣减 + 1次回滚)
if usedstore ~= nil and usedstore == 0 then
    return -1
end
return usedstore

5. 库存回滚(业务异常时恢复库存),若当前库存有效(大于等于0),则执行自增,返回结果值

local key = KEYS[1]
local usedstore = tonumber(redis.call('get', key))
if usedstore ~= nil and usedstore > -1 then
    local current = tonumber(redis.call('incr', key))
    return current
end
return usedstore

(2)文件Loader

读取.lua文件,将命令读取为字符串

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.net.URL;

/**
 * @author A
 * @description 文件加载工具类
 * @date 2019/12/2
 */
@Slf4j
public class FileLoader {
    public static String loadFile(String fileName) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        URL url = loader.getResource(fileName);
        if (url == null) {
            log.error("Cannot_locate_" + fileName + "_as_a_classpath_resource.");
            throw new RuntimeException("Cannot_locate_" + fileName + "_as_a_classpath_resource.");
        }

        try {
            return IOUtils.toString(url, "UTF-8");
        } catch (IOException e) {
            log.error("Cannot_read_" + fileName + "_content.");
            throw new RuntimeException("Cannot_read_" + fileName + "_content.");
        }
    }
}

(3)LuaScript类

将Lua命令字符串缓存,方便取出执行

import lombok.extern.slf4j.Slf4j;


public class LuaScript {

    // 常用原子操作
    private static final String INCR_EXPIRE;
    private static final String DECR_EXPIRE;
    private static final String SETNX_EXPIRE;
    private static final String HSET_EXPIRE;
    private static final String INCRBY_EXPIRE;
    private static final String SET_EXPIRE;

    // 库存操作
    private static final String STOCK_DEDUCT;
    private static final String STOCK_ROLLBACK;

    static {
        DECR_EXPIRE = FileLoader.loadFile("lua/decr_expire.lua");
        INCR_EXPIRE = FileLoader.loadFile("lua/incr_expire.lua");
        SETNX_EXPIRE = FileLoader.loadFile("lua/setnx_expire.lua");
        HSET_EXPIRE = FileLoader.loadFile("lua/hset_expire.lua");
        INCRBY_EXPIRE = FileLoader.loadFile("lua/incrby_expire.lua");
        SET_EXPIRE = FileLoader.loadFile("lua/set_expire.lua");
        //
        STOCK_DEDUCT = FileLoader.loadFile("lua/stock_deduct.lua");
        STOCK_ROLLBACK = FileLoader.loadFile("lua/stock_rollback.lua");
    }

    public static String incrExpireScript() {
        return INCR_EXPIRE;
    }

    public static String decrExpireScript() {
        return DECR_EXPIRE;
    }

    public static String setnxExpireScript() { return SETNX_EXPIRE; }

    public static String hsetExpireScript() { return HSET_EXPIRE; }

    public static String incrbyExpireScript() {
        return INCRBY_EXPIRE;
    }

    public static String setExpireScript() {
        return SET_EXPIRE;
    }

    public static String deductStock() { return STOCK_DEDUCT; }

    public static String rollbackStock() {
        return STOCK_ROLLBACK;
    }
}

(4)redisSevice

封装reids基本命令,可以加一些重试、日志、异常处理

public interface RedisService {

    /**
     * 获取缓存
     *
     * @param key
     * @return
     */
    String get(String key);

    /**
     * 设置缓存并加上过期时间
     *
     * @param key
     * @param value
     * @param ttl
     * @return
     */
    boolean setByExpire(String key, String value, String ttl);

    boolean setByExpire(String key, String value, int ttl);

    boolean hsetExpire(String key, String field, String value, String ttl);

    /**
     * 删除缓存
     *
     * @param key
     * @return
     */
    void delete(String key);

    /**
     * 自增,初始化时设置失效时间
     *
     * @param key
     * @param ttl
     * @return
     */
    long incrByExpire(String key, String ttl);

    /**
     * hdel
     *
     * @param key
     * @param field
     */
    void hdel(String key, String field);
}
@Slf4j
@Service
public class RedisServiceImpl implements RedisService {

    @Resource
    private JedisClusterTemplate jedisClusterTemplate;

    @Override
    public String get(String key) {
        try {
            return jedisClusterTemplate.get(key);
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]get_data_exception! [key={}]", key);
        }
        return null;
    }

    @Override
    public boolean setByExpire(String key, String value, String ttl) {
        try {
            long ok = (long) jedisClusterTemplate.eval(LuaScript.setExpireScript(), Collections.singletonList(key),
                    Arrays.asList(ttl, value));
            if (ok == 0) {
                log.error("[SERIOUS_REDIS]setByExpire_error! [key={}]", key);
                return false;
            }
            return true;
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]setByExpire_exception! e:{}", e);
        }
        return false;
    }

    @Override
    public boolean setByExpire(String key, String value, int ttl) {
        return setByExpire(key, value, String.valueOf(ttl));
    }

    @Override
    public boolean hsetExpire(String key, String field, String value, String ttl) {
        try {
            jedisClusterTemplate.eval(LuaScript.hsetExpireScript(), Arrays.asList(key),
                    Arrays.asList(field, ttl, value));
            return true;
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]hsetExpire_exception! [key={}]", key);
        }
        return false;
    }

    @Override
    public void delete(String key) {
        try {
            jedisClusterTemplate.del(key);
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]delete_error! e:{}", e);
        }
    }

    @Override
    public long incrByExpire(String key, String ttl) {
        try {
            long val = (long) jedisClusterTemplate.eval(LuaScript.incrExpireScript(), Collections.singletonList(key),
                    Collections.singletonList(ttl));
            if (val == 1) {
                log.info("incrByExpire_initially_set_ttl [key={} ttl={}]", key, ttl);
            }
            return val;
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]incrByExpire_exception! [key={}]", key);
        }
        return 0L;
    }

    @Override
    public void hdel(String key, String field) {
        try {
            jedisClusterTemplate.hdel(key, field);
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]hdel_error.[key={}]", key);
        }
    }
}

(5)示例,场景为会员资格以带库存的奖品的形式发放

@Override
    public void sendMemberAward(AwardSendReqDTO awardSendReqDTO) {
        Long awardId = awardSendReqDTO.getAwardId();
        Integer productId = awardSendReqDTO.getProductId();
        // 校验奖品产品id
        Integer bizType = Objects.requireNonNull(ProductTypeEnum.getBizTypeByProductId(productId), "bizType_is_null![productId=" + productId + "]");
        // 查询发放前的用户会员信息
        MemberDTO memberBefore = memberService.queryMemberDTO(awardSendReqDTO.getOpenid(), bizType);
        // 校验当前用户购买上限
        checkMemberMaxValidDays(memberBefore);

        // 1.先库存扣减
        long residualTemp = deductResidual(awardId);
        log.warn("[award send]residual_pre_deduct. [awardId={} residualTemp={}]", awardId, residualTemp);
        if (residualTemp < 0) {
            // 扣减失败不需要回滚库存
            throw new BusinessException(ResultCodeEnum.AWARD_INSUFFICIENT);
        }
        // 2.发放记录先入库
        AwardSendRecordDO sendRecord = buildSendRecord(awardSendReqDTO);
        try {
            awardSendRecordDAO.insertRecord(sendRecord);
        } catch (DuplicateKeyException e) {
            // 库存回滚
            rollbackResidual(awardId);
            log.warn("[award send]sendRecord_intoDB_duplicate_data. sendRecord={}", sendRecord);
            throw new BusinessException(ResultCodeEnum.AWARD_SEND_REPEAT);
        } catch (DataIntegrityViolationException e) {
            rollbackResidual(awardId);
            log.warn("[award send]sendRecord_intoDB_duplicate_data. sendRecord={}", JSON.toJSONString(sendRecord));
            throw new BusinessException(ResultCodeEnum.AWARD_SEND_REPEAT);
        } catch (Exception e) {
            rollbackResidual(awardId);
            log.error("[SERIOUS_DB][award send]sendRecordIntoDB_error! sendRecord={} e:{}", JSON.toJSONString(sendRecord), e);
            throw new BusinessException(ResultCodeEnum.SERVER_ERROR);
        }
        // 3.更新会员信息
        try {
            // 流水单号orderNo:"商户单号:appId"
            String orderNo = String.join(ApplicationConstants.SEPARATOR, awardSendReqDTO.getAppId(), awardSendReqDTO.getRequestNo());
            memberService.updateMemberInfoAfterPay(awardSendReqDTO.getOpenid(), orderNo, sendRecord.getProductId(), null, null, MemberGainTypeEnum.AWARD);
            // 奖品气泡提醒
            redisService.setByExpire(CacheKeyUtil.getAwardBubbleKey(awardSendReqDTO.getOpenid()), "1", ConfigManager.getInteger(ApplicationConstants.NEW_MEMBER_REMIND_REDIS_TTL, ApplicationConstants.NEW_MEMBER_REMIND_REDIS_TTL_DEFAULT));
        } catch (Exception e) {
            rollbackResidual(awardId);
            // 删除发放记录
            awardSendRecordDAO.deleteByPrimaryKey(sendRecord.getId());
            log.error("[award send]sendMemberAward.updateMemberInfo_error! awardSendRecord_rollback_suc. [awardSendReqDTO={}]", awardSendReqDTO);
            throw new BusinessException(ResultCodeEnum.SERVER_ERROR);
        }
        // 4.发放权益
        benefitTaskService.sendBenefitForNewMemberPeriod(awardSendReqDTO.getOpenid(), bizType, null);
        // 5.奖品到账提醒(异步)
        if (ApplicationConstants.AWARD_SEND_SYSTEM_NOTIFY_ON.equals(awardSendReqDTO.getNotifyType())) {
            CompletableFuture.runAsync(() -> this.sendAwardRcvNotifyMsg(awardSendReqDTO.getOpenid(), memberBefore, productId));
        }
    }

扣减库存:

    /**
     * 扣减库存(-1),返回扣减后结果
     *
     * @param awardId
     * @return
     */
    private Long deductResidual(Long awardId) {
        String key = CacheKeyUtil.getAwardTemplateResidualKey(String.valueOf(awardId));
        try {
            // 只对>0的库存做扣减,否则直接返回'-1/失败'; redis数据丢失时返回null;
            Long residualTemp = (Long) jedisClusterTemplate.eval(LuaScript.deductStock(), Collections.singletonList(key), Lists.newArrayList());
            if (residualTemp == null) {
                // redis重启时,刷新一下库存缓存,重新执行库存-1
                Long currentResidual = this.freshResidual(awardId);
                log.warn("[award send]awardResidual_cache_init. [awardId={} currentResidual={}]", awardId, currentResidual);
                residualTemp = (Long) jedisClusterTemplate.eval(LuaScript.deductStock(), Collections.singletonList(key), Lists.newArrayList());
                return residualTemp;
            }
            return residualTemp;
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]deduct_residual_error! e:{}", e);
            // 异常时会库存回滚库存
            throw new RuntimeException();
        }
    }

回滚库存:

    /**
     * 回滚库存缓存,业务异常时触发,返回回滚后库存
     *
     * @param awardId
     * @return
     */
    private Long rollbackResidual(Long awardId) {
        String key = CacheKeyUtil.getAwardTemplateResidualKey(String.valueOf(awardId));
        try {
            // 只对>-1的库存执行+1,否则直接返回'-1/失败'
            Long residualTemp = (Long) jedisClusterTemplate.eval(LuaScript.rollbackStock(), Collections.singletonList(key), Lists.newArrayList());
            if (residualTemp == null || residualTemp < 1) {
                // redis重启时刷新最新库存
                Long currentResidual = this.freshResidual(awardId);
                log.warn("[award send]awardResidual_cache_init. [awardId={} currentResidual={}]", awardId, currentResidual);
                return currentResidual;
            }
            return residualTemp;
        } catch (Exception e) {
            log.error("[SERIOUS_REDIS]rollback_residual_error! e:{}", e);
            throw new RuntimeException();
        }
    }

刷新当前库存(可在异常时执行):

    @Override
    public Long freshResidual(Long awardId) {
        log.warn("called_at_AwardSendService.freshResidual(awardId={})", awardId);

        //加分布式锁 key=lock+awardId
        String lockName = String.join(":", DistributedLockConstants.LOCK_REFRESH_CACHE_RESIDUAL, String.valueOf(awardId));
        int lockTimeoutSecond = ConfigManager.getInteger(DistributedLockConstants.DISTRIBUTED_LOCK_TIMEOUT_KEY, DistributedLockConstants.DEFAULT_DISTRIBUTED_LOCK_TIMEOUT);

        return distributedLockTemplate.execute(lockName, lockTimeoutSecond, new DistributedLockTemplate.BusinessHandler() {
            @Override
            public Long onLocked() {
                //获得锁 执行逻辑
                log.info("get_distributed_lock_then_begin_process! [awardId={}]", awardId);
                try {
                    String key = CacheKeyUtil.getAwardTemplateResidualKey(String.valueOf(awardId));
                    // 先查一下 若已经刷上库存 则直接返回当前库存
                    String curResidual = jedisClusterTemplate.get(key);
                    if (StringUtils.isNotBlank(curResidual)) {
                        return Long.valueOf(curResidual);
                    }

                    AwardTemplateDO awardTemplateDO = awardTemplateDAO.selectByPrimaryKey(awardId);
                    if (awardTemplateDO == null) {
                        return null;
                    }
                    Long sentNum = awardSendRecordDAO.countSentById(awardId);
                    long currentResidual = awardTemplateDO.getQuantity() - sentNum;
                    jedisClusterTemplate.set(key, String.valueOf(currentResidual));
                    log.warn("[award send]awardResidual_cache_fresh_success. [awardId={} currentResidual={}]", awardId, currentResidual);
                    return currentResidual;
                } catch (Exception e) {
                    log.error("[SERIOUS_REDIS]awardResidual_cache_fresh_error! e:{}", e);
                    // 当前库存获取失败 直接抛出异常
                    throw new RuntimeException();
                }
            }

            @Override
            public Long onTimeout() {
                log.info("get_distributed_lock_timeout! [awardId={}]", awardId);
                // 超时直接抛异常
                throw new RuntimeException();
            }
        });

    }

你可能感兴趣的:(代码技巧,Redis,java)