demo包括lua脚本文件、文件读入、redis命令执行脚本;
(1)Lua脚本
位置放在resource目录下:
脚本较多,列举几个:
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<Long>() {
@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();
}
});
}