Redis - 一个简单的抢红包小项目
1.项目分析
抢红包功能在如今已经是一个社交产品不可或缺的功能了,包括微信、支付宝等等各大厂商软件都实现了抢红包的这个功能。实现抢红包的方式有很多种,但其实这也是属于
Redis
的一个比较常见的应用场景。这里我们就围绕着Redis
技术来实现抢红包这个功能。
实现抢红包这个功能,我们就得先分析清楚整个业务。相信大家使用过红包功能的都知道,整个业务其实主要分几个场景:发红包(新建红包)
、抢红包(检验红包状态)
、拆红包
。这三个场景是目前大多数红包功能的流程,其实在之前早些时候不知道大家是否有印象,抢红包和拆红包属于一个步骤,现拆分成两个场景也可以一定程度上分散接口压力。
为了之后能更好更完善地编码,这里我们对整个流程详细分析一遍,三个业务场景对应了我们三个接口,这里我们对每个场景如何实现进行一个稍微详细的分析。
- 发红包(新建红包):在项目启动时,我们会提前通过
Redis
布隆过滤器将用户标识信息加载好,这里对布隆过滤器不清楚的同学可以看下【Redis - 布隆过滤器】,当用户进行发红包操作时,我们会先在布隆过滤器中检测当前操作用户是否属于真实用户,这样也可以过滤一部分恶意请求。然后我们会将用户信息以及生成的红包信息(红包金额、红包数量)等新增到DB中,并同时将红包所需相关信息(红包剩余金额、红包剩余数量)放入Redis
缓存中。- 抢红包(检验红包状态):这个场景就属于比较简单的一个场景了,主要就是用来检验红包目前状态(剩余金额、剩余数量)是否可抢,整个流程完整走通这个场景可以为后面业务抵挡一部分压力。
- 拆红包:这个场景可以说是整个红包功能的核心场景,也是实现起来需要考虑细节最多的地方。当用户进行拆红包操作时,我们依然会先检测红包的可用状态,然后我们还需要去检验当前用户是否已经抢过当前红包。如果所有校验都通过则通过抢红包金额算法去对红包进行操作,同时DB对红包信息以及抢红包记录信息进行更新,整个拆红包流程为了保证并发操作同一个红包时数据不一致导致的各种问题,这里我引用了分布式锁去控制【Redis - 分布式锁实现以及相关问题解决方案】。
ps:发红包过程中红包的标识ID生成我们使用的是UUID去生成,这里如果想做得更完善,可以参考其他分布式ID生成方案,例如雪花算法等生成分布式唯一ID。另外在拆红包实现时,其实我们也应该通过布隆过滤器对用户进行校验,这里我们做的小项目有一些地方没有完善,大家可以之后自己去完善。以及我们拆红包过程中使用了分布式锁去保证同一个红包的并发时的数据正确性,若不使用分布式锁在操作DB时使用CAS方式操作DB也可行。
上面就是拆红包的一个大体流程,再细化的逻辑需要根据具体业务需求来制定,下面我们来实现这个简单的抢红包小项目。
2.项目准备
这里我们首先需要一张用户信息相关的表用来作为抢红包的用户信息基础,我们就不另外创建,使用之前【Redis - 一个简单的排行榜小项目】项目中的用户积分表
user_score
中的信息。这里我们主要需要新建两张表,一张用于记录红包信息的表还有一张用于记录用户抢红包的流水表。
CREATE TABLE `red_packet_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`red_packet_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '红包id,采用timestamp+5位随机数',
`total_amount` int(11) NOT NULL DEFAULT '0' COMMENT '红包总金额,单位分',
`total_packet` int(11) NOT NULL DEFAULT '0' COMMENT '红包总个数',
`remaining_amount` int(11) NOT NULL DEFAULT '0' COMMENT '剩余红包金额,单位分',
`remaining_packet` int(11) NOT NULL DEFAULT '0' COMMENT '剩余红包个数',
`uid` varchar(32) NOT NULL DEFAULT '0' COMMENT '新建红包用户的用户标识',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_packet_id` (`red_packet_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='红包信息表,新建一个红包插入一条记录';
CREATE TABLE `red_packet_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` int(11) NOT NULL DEFAULT '0' COMMENT '抢到红包的金额',
`nick_name` varchar(32) DEFAULT '0' COMMENT '抢到红包的用户的用户名',
`img_url` varchar(255) DEFAULT '0' COMMENT '抢到红包的用户的头像',
`uid` varchar(32) NOT NULL DEFAULT '0' COMMENT '抢到红包用户的用户标识',
`red_packet_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '红包id,采用timestamp+5位随机数',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COMMENT='抢红包记录表,抢一个红包插入一条记录';
这里我们同样通过
mybatis-generator
去生成对应的Entity
和Mapper
,快速进行项目核心接口的编写。
3.项目编写
这里我们主要涉及三个接口:用户发红包、用户抢红包、用户拆红包,这里我们先贴出代码再把整个流程走一遍。
RedPacketController.java
这里首先是我们提供API入口Controller,包含上面提及的三个API接口。
package com.springboot.controller;
import com.springboot.common.response.ResponseData;
import com.springboot.common.response.ResponseUtils;
import com.springboot.service.RedPacketService;
import org.apache.tomcat.util.http.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author hzk
* @date 2019/7/24
*/
@RestController
@RequestMapping("/api/redpacket")
public class RedPacketController {
@Autowired
private RedPacketService redPacketService;
/**
* 用户发红包
* @param uid 用户ID
* @param totalNum 红包总数
* @param totalAmount 红包总额
*/
@RequestMapping("/addRedPacket/{uid}/{totalNum}/{totalAmount}")
public ResponseData addRedPacket(@PathVariable("uid") String uid,@PathVariable("totalNum") Integer totalNum,
@PathVariable("totalAmount") Integer totalAmount){
return ResponseUtils.response(redPacketService.addRedPacket(uid,totalNum,totalAmount));
}
/**
* 用户抢红包
* @param redPacketId 红包ID
*/
@RequestMapping("/getRedPacket/{redPacketId}")
public ResponseData getRedPacket(@PathVariable("redPacketId") String redPacketId){
return ResponseUtils.response(redPacketService.getRedPacket(redPacketId));
}
/**
* 用户拆红包
* @param redPacketId 红包ID
*/
@RequestMapping("/unpackRedPacket/{redPacketId}/{uid}")
public ResponseData unpackRedPacket(@PathVariable("redPacketId") String redPacketId,@PathVariable("uid") String uid){
return ResponseUtils.response(redPacketService.unpackRedPacket(redPacketId,uid));
}
}
RedPacketService.java
package com.springboot.service;
import com.springboot.common.response.ServiceResponse;
/**
* @author hzk
* @date 2019/7/24
*/
public interface RedPacketService {
/**
* 用户新增红包
* @param uid 用户ID
* @param totalNum 红包总数
* @param totalAmount 红包总额
*/
ServiceResponse addRedPacket(String uid, Integer totalNum, Integer totalAmount);
/**
* 用户抢红包
* @param redPacketId 红包ID
*/
ServiceResponse getRedPacket(String redPacketId);
/**
* 用户拆红包
* @param redPacketId 红包ID
* @param uid 用户ID
*/
ServiceResponse unpackRedPacket(String redPacketId, String uid);
}
RedPacketServiceImpl.java
这里就是我们三个业务场景实际的实现过程,这里稍微解释下代码逻辑。
- 发红包(addRedPacket):这里我们首先程序启动时会初始化一个用户信息相关的布隆过滤器,这里我们会先检验当前操作用户是否可能存在于布隆过滤器中,然后通过校验后将生成的红包相关信息存入DB中,并将需要信息(红包剩余金额、红包剩余䩔)同步到
Redis
。- 抢红包(getRedPacket):这步相对来说是逻辑最简单的一步,只需将
Redis
中对应红包的剩余信息进行判别即可,这一步无需考虑并发性,采取一些排队或者加锁的策略,因为下一步依然会去校验红包真实状态如何。这一步在红包没有剩余时,也能一定程度上减少后续接口的压力。- 拆红包(unpackRedPacket):这一步可谓是整个红包流程中最核心的一步,也是具体实现最需要认真设计思考的一步。首先我们就要考虑到并发性,当同一个红包被多个用户同时点击时,程序并发执行会导致所有用户在某一时刻获取红包状态一致,可能会出现秒杀架构中超卖的现象,这里我们采用了
Redis
分布式锁去进行并发控制,不是很清楚Redis
分布式锁的同学可以看看【Redis - 分布式锁实现以及相关问题解决方案】,对于锁粒度的掌控也是十分精妙的,粒度太细则会导致性能下降严重,粒度太粗会导致没有达到理想效果,这里我们只需对具体到某个红包进行粒度掌控即可。接着我们需要再次校验红包剩余情况以及用户是否已经抢过当前红包,若所有条件满足则通过特定红包算法对红包进行decr by
以及DB中红包信息更新、红包流水表新增记录等操作。
package com.springboot.service.impl;
import com.springboot.common.response.ResponseCodeEnum;
import com.springboot.common.response.ServiceResponse;
import com.springboot.dao.RedPacketInfoMapper;
import com.springboot.dao.RedPacketRecordMapper;
import com.springboot.repository.entity.RedPacketInfo;
import com.springboot.repository.entity.RedPacketInfoExample;
import com.springboot.repository.entity.RedPacketRecord;
import com.springboot.repository.entity.RedPacketRecordExample;
import com.springboot.service.RedPacketService;
import com.springboot.service.RedisService;
import com.springboot.service.UserBloomFilterService;
import com.springboot.task.RedisExpandLockExpireTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
import java.util.Random;
/**
* @author hzk
* @date 2019/7/24
*/
@Service
public class RedPacketServiceImpl implements RedPacketService{
@Autowired
private UserBloomFilterService userBloomFilterService;
@Autowired
private RedisService redisService;
@Autowired
private RedPacketInfoMapper redPacketInfoMapper;
@Autowired
private RedPacketRecordMapper redPacketRecordMapper;
public static final String REMAINING_NUM = ":remaining_num";
public static final String REMAINING_AMOUNT = ":remaining_amount";
public static final String UNPACK_LOCK_PRE = "unpack_lock_prefix_";
@Value("${server.port}")
private String port;
@Override
public ServiceResponse addRedPacket(String uid, Integer totalNum, Integer totalAmount) {
ServiceResponse serviceResponse = new ServiceResponse();
Date currentDate = new Date(System.currentTimeMillis());
//校验当前用户是否是真实用户
Boolean userExist = userBloomFilterService.bloomFilterExist(uid);
if(!userExist){
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.USER_NOTEXIST_EXCEPTION.getCode());
serviceResponse.setMsg(ResponseCodeEnum.USER_NOTEXIST_EXCEPTION.getDescribe());
return serviceResponse;
}
RedPacketInfo redPacketInfo = new RedPacketInfo();
//这里使用简单的UUID方式生成ID,分布式唯一键建议大家可以了解其他方式生成,例如雪花算法等
redPacketInfo.setRedPacketId(StringUtils.replace(java.util.UUID.randomUUID().toString(), "-", "").toUpperCase());
redPacketInfo.setTotalAmount(totalAmount);
redPacketInfo.setTotalPacket(totalNum);
redPacketInfo.setRemainingAmount(totalAmount);
redPacketInfo.setRemainingPacket(totalNum);
redPacketInfo.setUid(uid);
redPacketInfo.setCreateTime(currentDate);
redPacketInfo.setUpdateTime(currentDate);
boolean result = saveOrUpdateRedPacketInfo(redPacketInfo,null);
serviceResponse.setSuccess(result);
if(result){
//将红包信息(剩余包数量、剩余包总额)添加到Redis中
redisService.set(redPacketInfo.getRedPacketId() + REMAINING_NUM,redPacketInfo.getRemainingPacket());
redisService.set(redPacketInfo.getRedPacketId() + REMAINING_AMOUNT,redPacketInfo.getRemainingAmount());
}
return serviceResponse;
}
@Override
public ServiceResponse getRedPacket(String redPacketId) {
ServiceResponse serviceResponse = new ServiceResponse();
serviceResponse.setSuccess(true);
Object remainingNumObj = redisService.get(redPacketId + REMAINING_NUM);
if(null == remainingNumObj){
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.RED_PACKET_NOTEXIST_EXCEPTION.getCode());
serviceResponse.setMsg(ResponseCodeEnum.RED_PACKET_NOTEXIST_EXCEPTION.getDescribe());
return serviceResponse;
}
int remainingNum = Integer.parseInt(String.valueOf(remainingNumObj));
if(remainingNum <= 0){
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.RED_PACKET_FINISH_EXCEPTION.getCode());
serviceResponse.setMsg(ResponseCodeEnum.RED_PACKET_FINISH_EXCEPTION.getDescribe());
return serviceResponse;
}
serviceResponse.setData(remainingNum);
return serviceResponse;
}
@Override
public ServiceResponse unpackRedPacket(String redPacketId, String uid) {
ServiceResponse serviceResponse = new ServiceResponse();
String lockName = UNPACK_LOCK_PRE + redPacketId;
String currentValue = redisService.getHostIp() + ":" + port;
Boolean luaResult = false;
Long expire = 60L;
try {
//设置锁
luaResult = redisService.luaScript(lockName,currentValue,expire);
if(luaResult){
//开启守护线程 定期检测 续锁 执行拆红包业务
serviceResponse = executeBusiness(lockName, currentValue, expire, redPacketId, uid);
}else{
//获取锁失败
String value = (String) redisService.get(lockName);
//校验锁内容 支持可重入性
if(currentValue.equals(value)){
Boolean expireResult = redisService.expire(lockName, expire);
if(expireResult){
serviceResponse = executeBusiness(lockName,currentValue,expire,redPacketId, uid);
}
}
System.out.println("Lock fail,current lock belong to:" + value);
}
}catch (Exception e){
System.out.println("unpackRedPacket exception:" + e);
}finally {
if(luaResult){
//若分布式锁Value与本机Value一致,则当前机器获得锁,进行解锁
Boolean releaseLock = redisService.luaScriptReleaseLock(lockName, currentValue);
if(releaseLock){
System.out.println("release lock success");
}else{
System.out.println("release lock fail");
}
}else{
//获取锁失败
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getCode());
serviceResponse.setMsg(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getDescribe());
}
}
return serviceResponse;
}
/**
* 执行拆红包业务
*/
private ServiceResponse executeBusiness(String lockName,String currentValue,Long expire,String redPacketId, String uid) throws InterruptedException {
System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
//开启守护线程 定期检测 续锁
RedisExpandLockExpireTask expandLockExpireTask = new RedisExpandLockExpireTask(lockName,currentValue,expire,redisService);
Thread thread = new Thread(expandLockExpireTask);
thread.setDaemon(true);
thread.start();
//执行拆红包业务
//检验红包是否存在以及是否还有剩余
ServiceResponse serviceResponse = getRedPacket(redPacketId);
if(!serviceResponse.isSuccess()){
return serviceResponse;
}
serviceResponse.setSuccess(true);
Integer remainingNum = (Integer) redisService.get(redPacketId + REMAINING_NUM);
Integer remainingAmount = (Integer) redisService.get(redPacketId + REMAINING_AMOUNT);
if(remainingNum <= 0 || remainingAmount <= 0){
//Redis中剩余金额或者数量为空
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getCode());
serviceResponse.setMsg(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getDescribe());
return serviceResponse;
}
//检验当前用户是否已经抢过该红包
RedPacketRecordExample redPacketRecordExample = new RedPacketRecordExample();
redPacketRecordExample.createCriteria().andUidEqualTo(uid).andRedPacketIdEqualTo(redPacketId);
List<RedPacketRecord> redPacketRecords = redPacketRecordMapper.selectByExample(redPacketRecordExample);
if(!CollectionUtils.isEmpty(redPacketRecords)){
//该用户已抢过当前红包
serviceResponse.setSuccess(false);
serviceResponse.setCode(ResponseCodeEnum.RED_PACKET_REPEAT_UNPACK_EXCEPTION.getCode());
serviceResponse.setMsg(ResponseCodeEnum.RED_PACKET_REPEAT_UNPACK_EXCEPTION.getDescribe());
return serviceResponse;
}
//Integer remainingNum = Integer.parseInt(remainingNumStr);
//Integer remainingAmount = Integer.parseInt(remainingAmountStr);
//红包金额算法,此处大家可以自己去更深入查阅一些关于红包算法各种实现的资料
Integer randomAmount;
if(remainingNum == 1){
//若为最后一个红包则拆得全部金额 并删除红包在缓存中数据
randomAmount = remainingAmount;
redisService.remove(redPacketId + REMAINING_NUM);
redisService.remove(redPacketId + REMAINING_AMOUNT);
}else{
Integer maxRandomAmount = remainingAmount / remainingNum * 2;
randomAmount = new Random().nextInt(maxRandomAmount);
redisService.decr(redPacketId + REMAINING_NUM,1);
redisService.decr(redPacketId + REMAINING_AMOUNT,randomAmount);
}
//添加拆红包记录 以及红包信息更新 (此处更完善需考虑操作DB是否成功 失败后如何处理)
Date currentDate = new Date(System.currentTimeMillis());
RedPacketRecord redPacketRecord = new RedPacketRecord();
redPacketRecord.setUid(uid);
redPacketRecord.setRedPacketId(redPacketId);
redPacketRecord.setCreateTime(currentDate);
redPacketRecord.setAmount(randomAmount);
saveOrUpdateRedPacketRecord(redPacketRecord);
RedPacketInfo redPacketInfo = new RedPacketInfo();
RedPacketInfoExample redPacketInfoExample = new RedPacketInfoExample();
redPacketInfo.setUpdateTime(currentDate);
redPacketInfo.setRemainingPacket(remainingNum - 1);
redPacketInfo.setRemainingAmount(remainingAmount - randomAmount);
redPacketInfoExample.createCriteria().andRedPacketIdEqualTo(redPacketId);
saveOrUpdateRedPacketInfo(redPacketInfo,redPacketInfoExample);
serviceResponse.setData(randomAmount);
return serviceResponse;
}
private boolean saveOrUpdateRedPacketInfo(RedPacketInfo redPacketInfo, RedPacketInfoExample redPacketInfoExample){
boolean result = false;
int flag;
if(null == redPacketInfo){
return result;
}
if(null != redPacketInfo.getId() || null != redPacketInfoExample){
if(null != redPacketInfoExample){
flag = redPacketInfoMapper.updateByExampleSelective(redPacketInfo,redPacketInfoExample);
}else{
flag = redPacketInfoMapper.updateByPrimaryKeySelective(redPacketInfo);
}
}else{
flag = redPacketInfoMapper.insertSelective(redPacketInfo);
}
if(flag > 0){
result = true;
}
return result;
}
private boolean saveOrUpdateRedPacketRecord(RedPacketRecord redPacketRecord){
boolean result = false;
int flag;
if(null == redPacketRecord){
return result;
}
if(null != redPacketRecord.getId()){
flag = redPacketRecordMapper.updateByPrimaryKeySelective(redPacketRecord);
}else{
flag = redPacketRecordMapper.insertSelective(redPacketRecord);
}
if(flag > 0){
result = true;
}
return result;
}
}
UserBloomFilterService.java
该类主要用于项目启动初始化用户信息到布隆过滤器以及提供后期是否存在于布隆过滤器校验等方法。
package com.springboot.service;
import com.springboot.dao.UserScoreMapper;
import com.springboot.repository.entity.UserScore;
import com.springboot.repository.entity.UserScoreExample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* @author hzk
* @date 2019/7/19
*/
@Service
public class UserBloomFilterService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserScoreMapper userScoreMapper;
public static final String BLOOMFILTER_NAME = "user_bloom";
/**
* 程序启动时初始化用户标识
*/
@PostConstruct
public void initUserBloomFilter(){
System.out.println("initUserBloomFilter start...");
List<UserScore> userScores = userScoreMapper.selectByExample(new UserScoreExample());
if(!CollectionUtils.isEmpty(userScores)){
userScores.forEach(userScore -> {
bloomFilterAdd(userScore.getUserId());
});
}
System.out.println("initUserBloomFilter end...");
}
/**
* 向布隆过滤器中添加元素
* @param uid
* @return
*/
public Boolean bloomFilterAdd(String uid){
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_add.lua")));
LuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<Object> params = new ArrayList<>();
params.add(BLOOMFILTER_NAME);
params.add(uid);
return (Boolean) redisTemplate.execute(LuaScript, params);
}
/**
* 检验元素是否可能存在于布隆过滤器中
* @param uid
* @return
*/
public Boolean bloomFilterExist(String uid){
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_exist.lua")));
LuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<Object> params = new ArrayList<>();
params.add(BLOOMFILTER_NAME);
params.add(uid);
return (Boolean) redisTemplate.execute(LuaScript, params);
}
}
bf_add.lua
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.ADD',bloomName,value)
return result
bf_exist.lua
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.EXISTS',bloomName,value)
return result
RedisService.java
这里主要是封装了用于操作
Redis
命令的以及实现分布式锁的一些方法。
package com.springboot.service;
import com.springboot.task.ExpandLockExpireTask;
import com.springboot.task.RedisExpandLockExpireTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author hzk
* @date 2019/7/1
*/
@Service
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
private static double size = Math.pow(2, 32);
private DefaultRedisScript<Boolean> lockLuaScript;
/**
* 读取缓存
* @param key
* @param offset
* @return
*/
public boolean getBit(String key, long offset) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.getBit(key, offset);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存(String)
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 设置失效时间
* @param key
* @param value
* @return
*/
public boolean setExpire(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 批量删除对应的key
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 删除对应的key
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的key
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存(String)
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 设置过期时间
* @param key
* @return
*/
public Boolean expire(String key,Long expire) {
return redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
/**
* 减少decr by
* @param key
* @param value
* @return
*/
public boolean decr(final String key, int value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.increment(key,-value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 执行lua脚本
* @param key
* @param value
* @return
*/
public Boolean luaScript(String key,String value,Long expire){
lockLuaScript = new DefaultRedisScript<Boolean>();
lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
lockLuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<Object> params = new ArrayList<>();
params.add(key);
params.add(value);
params.add(String.valueOf(expire));
return (Boolean) redisTemplate.execute(lockLuaScript, params);
}
/**
* 执行lua脚本(释放锁)
* @param key
* @param value
* @return
*/
public Boolean luaScriptReleaseLock(String key,String value){
lockLuaScript = new DefaultRedisScript<Boolean>();
lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua")));
lockLuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<Object> params = new ArrayList<>();
params.add(key);
params.add(value);
return (Boolean) redisTemplate.execute(lockLuaScript, params);
}
/**
* 执行lua脚本(锁续时)
* @param key
* @param value
* @param expire
* @return
*/
public Boolean luaScriptExpandLockExpire(String key,String value,Long expire){
lockLuaScript = new DefaultRedisScript<Boolean>();
lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("expand_lock_expire.lua")));
lockLuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList<Object> params = new ArrayList<>();
params.add(key);
params.add(value);
params.add(String.valueOf(expire));
return (Boolean) redisTemplate.execute(lockLuaScript, params);
}
/**
* 获取本机内网IP地址方法
* @return
*/
public static String getHostIp(){
try{
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
}
setnx_ex.lua
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local expire = KEYS[3]
local result_nx = redis.call('SETNX',lockKey,lockValue)
if result_nx == 1 then
local result_ex = redis.call('EXPIRE',lockKey,expire)
return result_ex
else
return result_nx
end
release_lock.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/3
-- Time: 18:31
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local result_get = redis.call('get',lockKey);
if lockValue == result_get then
local result_del = redis.call('del',lockKey)
return result_del
else
return false;
end
expand_lock_expire.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/4
-- Time: 15:19
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local expire = KEYS[3]
local result_get = redis.call('GET',lockKey);
if lockValue == result_get then
local result_expire = redis.call('EXPIRE',lockKey,expire)
return result_expire
else
return false;
end
RedisExpandLockExpireTask.java
之前看过分布式锁文章的同学应该记得,这里是用于解决分布式锁实现中会出现的一些问题所自己封装的自动续锁的任务。
package com.springboot.task;
import com.springboot.service.RedisService;
/**
* @author hzk
* @date 2019/7/25
*/
public class RedisExpandLockExpireTask implements Runnable{
private String key;
private String value;
private long expire;
private boolean isRunning;
private RedisService redisService;
public RedisExpandLockExpireTask(String key, String value, long expire, RedisService redisService) {
this.key = key;
this.value = value;
this.expire = expire;
this.redisService = redisService;
this.isRunning = true;
}
@Override
public void run() {
//任务执行周期
long waitTime = expire * 1000 * 2 / 3;
while (isRunning){
try {
Thread.sleep(waitTime);
if(redisService.luaScriptExpandLockExpire(key,value,expire)){
System.out.println("Lock expand expire success! " + value);
}else{
stopTask();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void stopTask(){
isRunning = false;
}
}
ResponseUtils.java
该类用于转换接口API返回标准化格式数据的工具类。
package com.springboot.common.response;
import org.apache.tomcat.util.http.ResponseUtil;
import org.springframework.validation.FieldError;
import java.util.List;
/**
* Response Util
* @author hzk
* @date 2019/07/24
*/
public class ResponseUtils {
/**
* 统一处理响应数据
* @param serviceResponse
* @return
*/
public static ResponseData response(ServiceResponse serviceResponse) {
if (serviceResponse.isSuccess()) {
return ResponseUtils.success(serviceResponse);
} else {
return ResponseUtils.businessError(serviceResponse);
}
}
public static ResponseData success(ServiceResponse serviceResponse) {
ResponseData responseData = new ResponseData();
if(null != serviceResponse.getMsg()){
responseData.setMsg(serviceResponse.getMsg());
}else{
responseData.setMsg(ResponseCodeEnum.SUCCESS.getDescribe());
}
responseData.setCode(ResponseCodeEnum.SUCCESS.getCode());
responseData.setSuccess(true);
responseData.setData(serviceResponse.getData());
return responseData;
}
public static ResponseData businessError(ServiceResponse serviceResponse) {
ResponseData resultData = new ResponseData();
resultData.setSuccess(false);
if(serviceResponse.getCode() > 0){
resultData.setCode(serviceResponse.getCode());
}else{
resultData.setCode(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getCode());
}
resultData.setMsg(serviceResponse.getMsg());
return resultData;
}
public static ResponseData businessError(List<FieldError> errors) {
ResponseData resultData = new ResponseData();
resultData.setSuccess(false);
resultData.setCode(ResponseCodeEnum.SERVICE_BUSINESS_ERROR.getCode());
StringBuilder stringBuilder = new StringBuilder();
if (errors != null) {
for (FieldError error : errors) {
stringBuilder.append(error.getField());
stringBuilder.append(":");
stringBuilder.append(error.getDefaultMessage());
stringBuilder.append(" ");
}
resultData.setMsg(stringBuilder.toString());
} else {
resultData.setMsg(ResponseCodeEnum.UNKNOWN_BUSINESS_ERROR.getDescribe());
}
return resultData;
}
}
ResponseCodeEnum.java
该类用于转换接口API返回标准化格式数据所需相关的
CODE
码以及相关提示信息的枚举类。
package com.springboot.common.response;
/**
* 接口响应结果CODE
* @author hzk
* @date 2019/07/24
*/
public enum ResponseCodeEnum {
/**
* 返回成功
*/
SUCCESS(1000, "OK"),
/**
* 全局异常
*/
GLOBAL_EXCEPTION(4000, "Global Exception"),
/**
* 用户不存在
*/
USER_NOTEXIST_EXCEPTION(4001, "用户不存在"),
/**
* 红包已抢完
*/
RED_PACKET_FINISH_EXCEPTION(4002, "红包已抢完"),
/**
* 红包不存在
*/
RED_PACKET_NOTEXIST_EXCEPTION(4003, "红包不存在"),
/**
* 不可重复拆红包
*/
RED_PACKET_REPEAT_UNPACK_EXCEPTION(4004, "不可重复拆红包"),
/**
* Service业务错误
*/
SERVICE_BUSINESS_ERROR(5000,"Service Business Error"),
/**
* 未知错误
*/
UNKNOWN_BUSINESS_ERROR(5001,"Unknown Business Error");
private int code;
private String describe;
ResponseCodeEnum(int code, String describe) {
this.code = code;
this.describe = describe;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getDescribe() {
return describe;
}
public void setDescribe(String describe) {
this.describe = describe;
}
}
4.项目验证
这里我们先给大家看下我们目前DB中存在的用户信息。
这里我让kobe这个大户先发一个50元5个的。
http://127.0.0.1:8081/api/redpacket/addRedPacket/1015ABDDC99543C3AABD6C55F0A3F309/5/50000
发送成功,这里我们来看一下DB以及Redis
中相关的红包信息。
这样我们的发红包流程就成功了,这里我们再检验一下抢红包是否逻辑正确。
http://127.0.0.1:8081/api/redpacket/getRedPacket/FBCFDCDD4A304595950E4D7A36930147
这里FBCFDCDD4A304595950E4D7A36930147
是我们刚才新建的红包,抢红包响应正常。D724CA378B66462981114C895F5ABA22
是一个不存在的红包,这里也会正确给出我们响应。抢红包流程正确,大家需要更细分可以自己补充。
接下来就是我们核心的拆红包接口,我们验证下是否正确。http://127.0.0.1:8081/api/redpacket/unpackRedPacket/FBCFDCDD4A304595950E4D7A36930147/F6A3AFEAE61142538748C912F6DCD4C6
。
这里我们可以看到,正常拆红包、重复拆红包以及红包已被拆完等场景都会给出不同的响应,整个流程正确,大家可以根据自己具体需求去增加需要的逻辑。这里我们最后再看一下DB和Redis
中的数据情况。
数据记录和及时清除也正常,整个项目到这里就结束了。
5.项目总结
其实整个项目若把所需知识点都弄清了之后实现起来还是十分简单的,主要在于整个业务的拆分和设计。其实这里我们只是简单实现了一些基本的功能,更详细的还要根据具体的业务需求来变更。这里主要几个技术点:布隆过滤器、分布式锁、Redis操作。这几个技术点是核心,整个业务围绕着这些来进行实现。另外在拆红包这个场景中,还可以通过队列等各种手段来削峰,控制最大的一个并发数。
另外像一些高并发的业务场景其实主要核心思想都是减少I/O性能的消耗,例如更多使用缓存而非DB,可以使用内存更佳。并且会存在很多有效的过滤,避免更多无效请求进入核心业务处理。同时对于使用的工具例如Redis
,都可以进行优化和性能扩展。
技术和业务是相辅相成的,业务是技术的具体实现,技术是业务的底盘根基,所以平时在需求建立设计系统功能的时候我们需要考虑更多的一些方面,才能使整个功能更丰富、系统更高效地去提供服务。