最近做了一个业务,类似于美团,饿了吗配送员抢单的功能,简单的总结一下
首先这个功能对于项目来说,并发量不是很大,但是勉强算一个高并发吧
抢单的功能,首先要保证一点,一个单子只能被一个人抢到,哪怕有10w人来抢,也只能有一个人抢到,这里就要用到锁了,当一个人抢到单子时,要立刻保证,别人抢不到,这里选用Redis做
分布式锁实现方式有很多,Redis,zk,等等,至于为什么选用redis,个人原因,对Redis熟悉一点,不管用什么实现,原理都是大同小异的
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下三个条件:
互斥性:只有一个客户端能持有锁。
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客 户端能加锁。
加锁和解锁的客户端相同:加锁和解锁必须是同一个客户端
实现流程:
获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返 回 1;
若 key 存在,则什么都不做,返回 0
在释放锁的时候进行判断
返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。 setex(key,value,expire)过期以秒为单位
这样采用setnx,setex有个小问题,因为命令时分开使用的,假如程序在两个命令中出现问题,很有可能出现死锁,所以我这里采用命令连用的方式
在释放锁的时候进行判断,只能被同一个客户端释放
整个流程包括了,判断,加锁,释放
所以这里用到了try{} catch {} finally{}
public Object deteOrder(RecordTableModel recordTableModel) {
String key=recordTableModel.getMess_id();
String value=recordTableModel.getMan_openid();
boolean nxRet=false;
try {
//返回为t时,获取锁成功,f获取失败
nxRet = redisServer.setLock(key,value);
if (!nxRet){
LOGGER.info("获取锁失败");
return ResultMessage.success(ResultCode.GRAB_NO,"已有人配送");
}else{
LOGGER.info("获取锁成功:"+key);
//一些业务处理
//放入队列中
redisServer.sendChannelMess(Const.CHANNEL,JSONObject.toJSONString(recordTableModel));
return ResultMessage.success(true);
}
}catch (Exception e){
LOGGER.info("错误:{}",e);
}finally {
//放锁时要进行判断,加锁,放锁必须同一个客户端
if (nxRet) {
redisServer.deleKey(key);
}
}
//加锁成功返回T
public Boolean setLock(String key,String value){
try{
Boolean result=(Boolean)redisTemplate.execute(new RedisCallback() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
//设置超时时间 超过时间自动解锁
return connection.set(key.getBytes(),value.getBytes(),Expiration.seconds(3600),RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
}catch (Exception e){
}
return false;
public void sendChannelMess(String channel,String message){
try{
redisTemplate.convertAndSend(channel,message);
}catch (Exception e){
LOGGER.info("错误:{}",e);
}
}
下面实现redis的发布和订阅
//redis的配置文件
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,String> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om=new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 初始化监听器
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// new PatternTopic("这里是监听的通道的名字") 通道要和发布者发布消息的通道一致
container.addMessageListener(listenerAdapter, new PatternTopic(Const.CHANNEL));
return container;
}
/**
* 绑定消息监听者和接收监听的方法
* @param redisReceiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver redisReceiver) {
// redisReceiver 消息接收者
// receiveMessage 消息接收后的方法
return new MessageListenerAdapter(redisReceiver, "receiveMessage");
}
/**
* 注册订阅者
* @param latch
* @return
*/
@Bean
RedisReceiver receiver(CountDownLatch latch) {
return new RedisReceiver(latch);
}
/**
* 计数器,用来控制线程
* @return
*/
@Bean
CountDownLatch latch() {
//指定了计数的次数 1
return new CountDownLatch(1);
}
}
//这里是收到通道的消息之后执行的方法
public void receiveMessage(String message) {
LOGGER.info("收到的消息:"+message);
//入库
try {
//入库等业务
}catch (Exception e){
LOGGER.info("errpr:"+e);
}
latch.countDown();
}
实现到这里,大体的流程都走完了,具体的业务实现就看个人需求了
个人第一次写这种技术文章,有哪些地方不够清楚,欢迎留言,后续补充