基于redis 和mysql 分布式锁的实现

本文详细介绍了如何在Spring Boot环境中利用Redis的setIfAbsent方法实现分布式锁,并提供了扩展实现。同时,文章还展示了如何结合MySQL进行锁的管理,包括获取与释放锁的类实现。在异常处理方面,记录了不同异常状态,如Redis和MySQL的异常情况。此外,还给出了基于注解的切面处理,实现了在多线程环境下对锁的控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式锁实现

实体

用于记录锁执行情况

package cn.picclife.authen.system.bean.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@Accessors(chain = true)
public class TaskBo {
    private String id;
    private String name;
    private String systemInfo;
    private String redisException;
    private String mysqlException;
    private String otherException;
    private Date   createTime;
    /**
     *  3:获取redis锁失败
     *  4:获取mysql锁失败
     *  1:获取redis锁成功
     *  2获取mysql锁成功
     *  5:释放redis锁失败
     *  6:释放mysql锁失败
     *  7:释放redis锁成功
     *  8: 释放mysql锁成功
     */
    private String status;
    public static void main(String[] args) {
        try {
            Integer j=null;
            int i=1/j;
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }
}

基于springboot redis setIfAbsent() 方法的扩展

package cn.picclife.authen.system.distributed;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@Component
public class CustomRedisUtil    {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public   boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
        Boolean result = (Boolean) stringRedisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer valueSerializer = stringRedisTemplate.getValueSerializer();
                RedisSerializer keySerializer = stringRedisTemplate.getKeySerializer();
                Object obj = connection.execute("set", keySerializer.serialize(key),
                        valueSerializer.serialize(value),
                        "NX".getBytes(StandardCharsets.UTF_8),
                        "EX".getBytes(StandardCharsets.UTF_8),
                        String.valueOf(exptime).getBytes(StandardCharsets.UTF_8));
                return obj != null;
            }
        });
        return result;
    }
}

redis获取锁与释放锁的类

package cn.picclife.authen.system.distributed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
 *  任务开始结束时间  保存任务记录以及异常,
 */
@Slf4j
@Component
public class RedisDistribute {
    @Autowired
    private CustomRedisUtil redisUtil;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
       /**
     * 
     * 
     * @param lockName 锁名称
     * @param acquireTimeout redis重试时间 (这个参数可以不用 当去掉while循环和睡眠的时候)
     * @param expireTime redis过期时间
     * @return
     */
    public String lockWithTimeOut(String lockName,long acquireTimeout,long expireTime){
        String retIdentifier=null;
            String identifier= UUID.randomUUID().toString();
            String lockKey="lock:"+lockName;
            int expire=(int)expireTime/1000;
            long end=System.currentTimeMillis()+acquireTimeout;
            while (System.currentTimeMillis()<end){
                if(redisUtil.setIfAbsent(lockKey,identifier,expire) ){
                    retIdentifier=identifier;
                    return retIdentifier;
                }
                try{
                    Thread.sleep(10*1000);
                }catch (InterruptedException interruptedException){
                    Thread.currentThread().interrupt();
                }
            }
        return retIdentifier;
    }
    /**
     *  释放锁 如果上面redis添加了失效时间,其实锁也就不需要主动释放了,不过也可以主动释放
     * @param lockName
     * @param identify
     * @return
     */
    public boolean releaseLock(String lockName,String identify){

        String lockKey="lock:"+lockName;
        boolean retFlag=false;
        stringRedisTemplate.watch(lockKey);
        while (true){
            if(identify.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                stringRedisTemplate.delete(lockKey);
                retFlag=true;
            }
            stringRedisTemplate.unwatch();
            break;
        }
        return retFlag;
    }

}

package cn.picclife.authen.system.bean.bo;
import cn.picclife.authen.common.utils.SystemUtils;
import lombok.Data;
import java.util.Date;
/**
 * 数据库分布式锁 khzsx_lock
 */
@Data
public class LockBo {
    public final static String LOCKED_STATUS = "1";
    public final static String UNLOCKED_STATUS = "0";
    private String id;
    /**
     * 保证lockname唯一 加主键是一种办法
     */
    private String lockName;
    private String   expireTime;
    private String status;
    public LockBo(String lockName, String expireTime, String status) {
        this.id= SystemUtils.getCompanyTransNo();
        this.lockName = lockName;
        this.expireTime = expireTime;
        this.status = status;
    }
}
//mapper 接口
package cn.picclife.authen.system.mapper;
import cn.picclife.authen.system.bean.bo.LockBo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LockMapper {
    LockBo findByLockName(String lockName);
    int deleteByLockName(String lockName);
    int updateByLockName(String lockName);
    int saveLock(LockBo lockBo);
}
//mapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.picclife.authen.system.mapper.LockMapper">

	<select id="findByLockName" resultType="cn.picclife.authen.system.bean.bo.LockBo" >

                  SELECT id as id, lock_name as lockName,expire_time as expireTime,status as status
                  FROM khzsx_lock WHERE  lock_name= #{lockName} LIMIT 1 for update;
    </select>
	<delete id="deleteByLockName" parameterType="string" >
		DELETE FROM khzsx_lock WHERE lock_name=#{lockName}
	</delete>
	<update id="updateByLockName" parameterType="string" >
   		<!--DELETE FROM khzsx_lock WHERE lock_name=#{lockName}-->
		update  khzsx_lock
		<set>
			<if test="status!=null and status!='' ">
				status = '0'
			</if>

		</set>
		WHERE     lock_name=#{lockName}
    </update>
	<insert id="saveLock" parameterType="cn.picclife.authen.system.bean.bo.LockBo">
		INSERT INTO khzsx_lock
		(
		id,
		lock_name,
		expire_time,
		status
		)
		values (
		#{id},
		#{lockName},
		#{expireTime},
		#{status}
		)
		on duplicate key update id=id
	</insert>
</mapper>

实现mysql锁

package cn.picclife.authen.system.distributed;

import cn.picclife.authen.common.utils.DateUtil;
import cn.picclife.authen.system.bean.bo.LockBo;
import cn.picclife.authen.system.mapper.LockMapper;
import com.alibaba.druid.util.StringUtils;
import com.google.inject.internal.cglib.proxy.$Factory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
@Component
public class SqlDistribute {
  
    @Autowired
    private LockMapper lockMapper;
    @Transactional
    public String lock(String lockName,Integer expiredSeconds){
        if (StringUtils.isEmpty(lockName)) {
            throw new NullPointerException();
        }
        LockBo lockBo=lockMapper.findByLockName(lockName);
        
       /** try{
            Thread.sleep(1*1000);
        }catch (InterruptedException interruptedException){
            Thread.currentThread().interrupt();
        }**/
        if(Objects.isNull(lockBo)){
            lockBo=new LockBo(lockName, DateUtil.parseDateToStr(this.addSeconds(new Date(),expiredSeconds),DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS),LockBo.LOCKED_STATUS);
            //此处保证lockname 唯一
            if(lockMapper.saveLock(lockBo)>0){
                return lockBo.getLockName();
            }
        }
        else{
            if(lockBo.getStatus()==LockBo.UNLOCKED_STATUS&&DateUtil.parseStrToDate(lockBo.getExpireTime(),DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS).after(new Date())){
                return null;
            }
            String expiredTime = lockBo.getExpireTime();
            Date now = new Date();
            if (DateUtil.parseStrToDate(expiredTime,DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS).before(now)) {
                lockMapper.deleteByLockName(lockName);
                lockBo.setExpireTime(DateUtil.parseDateToStr(this.addSeconds(now, expiredSeconds),DateUtil.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI_SS));
                lockBo.setStatus(LockBo.LOCKED_STATUS);
                lockMapper.saveLock(lockBo);
                return lockBo.getLockName();
            }
        }
        return null;
    }
    public boolean releaseLock(String lockName){
        if (StringUtils.isEmpty(lockName)) {
            throw new NullPointerException();
        }
        int i= lockMapper.updateByLockName(lockName);
        return i==1?true:false;
    }
    private Date addSeconds(Date date, Integer seconds) {
        if (Objects.isNull(seconds)){
            seconds = DEFAULT_EXPIRED_SECONDS;
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.SECOND, seconds);
        return calendar.getTime();
    }
}

注解

package cn.picclife.authen.system.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
    String name () default "default";
    long expireTime () default 20*1000;
}

切面

package cn.picclife.authen.system.aspect;

import cn.picclife.authen.common.annotation.DatasourceChange;
import cn.picclife.authen.common.utils.SystemUtils;
import cn.picclife.authen.system.annotation.DistributedLock;
import cn.picclife.authen.system.bean.bo.TaskBo;
import cn.picclife.authen.system.distributed.RedisDistribute;
import cn.picclife.authen.system.distributed.SqlDistribute;
import cn.picclife.authen.system.mapper.TaskMapper;
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;


@Component
@Aspect
@Slf4j
public class DistributeAspect {

    @Autowired
    private RedisDistribute redisDistribute;
    @Autowired
    private SqlDistribute sqlDistribute;
    @Autowired
    private TaskMapper taskMapper;
    @Before ("@annotation(cn.picclife.authen.system.annotation.DistributedLock)")
    public void hh(JoinPoint point){
        System.out.println("======sssssssssssssssss=======================");

    }


    @Around("@annotation(cn.picclife.authen.system.annotation.DistributedLock)")
    public void  beforeStartTask(ProceedingJoinPoint pjp) throws NoSuchMethodException {
        InetAddress address=null;
        String ipAddr="";
        try {
            address= InetAddress.getLocalHost();
            ipAddr= address.getHostAddress();
        } catch (UnknownHostException e) {
            ipAddr="获取ip失败";
            log.error("获取系统ip异常"+e.getStackTrace());
        }
        TaskBo taskBo=new TaskBo();
        //获取该注解所在类的类名(此处指的是TestTask )
        Class<?> className = pjp.getTarget().getClass();
        //获取该注解所在类的方法名(此处指的是TestTask中的 test)
        String methodName = pjp.getSignature().getName();
        Class<?>[] argClass = ((MethodSignature) pjp.getSignature()).getParameterTypes();
        Method method = className.getMethod(methodName, argClass);
        boolean sqlflag=false;boolean redisflag=false;
        //Annotation[] annotations = className;
        if (method.isAnnotationPresent(DistributedLock.class)) {
            for (Annotation anno : method.getDeclaredAnnotations()) {{
                if(anno.annotationType().equals(DistributedLock.class)){
                    DistributedLock distributedLock = (DistributedLock)anno;
                    String name=distributedLock.name();
                    long expireTime=distributedLock.expireTime();
                    taskBo.setId(SystemUtils.getCompanyTransNo()).setName(name).setSystemInfo(ipAddr);
                    //taskMapper.saveTask(taskBo);
                    String isLock= null;
                    try {
                        isLock = redisDistribute.lockWithTimeOut(name,2000,expireTime);
                    } catch (Exception e) {
                        log.error("redis异常"+e );
                        log.info("<========使用mysql获取锁=======>");
                        taskBo.setRedisException(e.toString());
                        redisflag=true;
                        taskBo.setStatus("3");
                       // taskMapper.saveTask(taskBo);
                        try {
                            sqlflag=true;
                            isLock=sqlDistribute.lock(name,(int)expireTime/1000);
                        } catch (Exception exception) {
                                taskBo.setMysqlException(exception.toString());
                                taskBo.setStatus("4");
                               // taskMapper.updateTask(taskBo);
                                log.error("mysql异常"+exception );
                        }
                    }
                    if(Objects.isNull(isLock)) {
                        log.info("redis或者mysql获取锁失败,不向下执行方法");
                        return  ;
                    }else {
                        log.info("获取到锁,执行定时任务");
                        if(!redisflag){
                            taskBo.setStatus("1");
                            taskMapper.saveTask(taskBo);
                        }if(sqlflag){
                            taskBo.setStatus("2");
                            taskMapper.saveTask(taskBo);
                        }
                        try {
                            Object result = pjp.proceed();
                        } catch (Throwable throwable) {
                            log.info("定时任务执行有误"+throwable);
                            taskBo.setOtherException(throwable.toString());
                            taskMapper.updateTask(taskBo);
                        }
                        try {
                            if(redisDistribute.releaseLock(name,isLock)){
                                log.info("redis释放锁成功");
                                taskBo.setStatus("7");
                                taskMapper.updateTask(taskBo);
                            }
                        } catch (Exception e) {
                            log.error("redis异常"+e );
                            taskBo.setRedisException(e.toString());
                            taskBo.setStatus("5");
                            taskMapper.updateTask(taskBo);
                            log.info("<========使用mysql释放锁=======>");
                            try {
                                if(sqlDistribute.releaseLock(name)){
                                    taskBo.setStatus("8");
                                    taskMapper.updateTask(taskBo);
                                    log.info("<========mysql释放锁成功=======>");
                                }
                            } catch (Exception exception) {
                                log.info("<========mysql释放锁失败联系管理员=======>");
                                log.error("mysql异常"+e);
                                taskBo.setStatus("6");
                                taskBo.setMysqlException(exception.toString());
                                taskMapper.updateTask(taskBo);
                            }
                        }
                    }
                }
            }
        }
    }
    }
   /* @After ("@annotation(cn.picclife.authen.system.annotation.DistributedLock)")
    public void afterEndTask(JoinPoint point)   {
        Class<?> className = point.getTarget().getClass();
        String methodName = point.getSignature().getName();
    }*/

}

main方法

package cn.picclife.authen.system.distributed;

import cn.picclife.authen.common.utils.SystemUtils;
import cn.picclife.authen.system.annotation.DistributedLock;
import cn.picclife.authen.system.bean.bo.LockBo;
import cn.picclife.authen.system.bean.bo.TaskBo;
import cn.picclife.authen.system.mapper.LockMapper;
import cn.picclife.authen.system.mapper.TaskMapper;
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
@Slf4j
public class TestTask  {
    @Autowired
    private RedisDistribute redisDistribute;
    @Autowired
    private SqlDistribute sqlDistribute;
    @Autowired
    private TaskMapper taskMapper;
    //@Scheduled(cron = "0 */1 *  * * * ")
    public void test(){
        log.info("==========开始执行test方法===============");
        CountDownLatch countDownLatch = new CountDownLatch(1);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0;i<10;i++){
                executorService.submit(()->{
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException interruptedException) {
                        interruptedException.printStackTrace();
                    }
                    //test1();

                    { boolean sqlflag=false;boolean redisflag=false;
                        TaskBo taskBo=new TaskBo();
                        String name="distributedLock";
                        long expireTime=300*1000;
                        taskBo.setId(SystemUtils.getCompanyTransNo()).setName(name).setSystemInfo("ipAddr");
                       // taskMapper.saveTask(taskBo);
                        String isLock= null;
                        try {
                            //TransientDataAccessResourceException: Error attempting to get column 'lock_name' from result set.  Cause: java.sql.SQLException: Cannot convert value 'distributedLock.name()' from column 2 to TIMESTAMP.
                            isLock = redisDistribute.lockWithTimeOut(name,2000,expireTime);
                        } catch (Exception e) {
                            log.error("redis异常"+e );
                            log.info("<========使用mysql获取锁=======>");
                            taskBo.setRedisException(e.toString());
                            redisflag=true;
                            try {
                                sqlflag=true;
                                isLock=sqlDistribute.lock(name,(int)expireTime/1000);

                            } catch (Exception exception) {
                                    taskBo.setMysqlException(exception.toString());
                                    taskBo.setStatus("4");
                                   // taskMapper.updateTask(taskBo);
                                    log.error("mysql异常"+exception );
                            }
                        }
                        if(Objects.isNull(isLock)) {
                            log.info("redis或者mysql获取锁失败,不向下执行方法");
                            return  ;
                        }else {
                            log.info("获取到锁,执行定时任务");
                            if(!redisflag){
                                taskBo.setStatus("1");
                                taskMapper.saveTask(taskBo);
                            }if(sqlflag){
                                taskBo.setStatus("2");
                                taskMapper.saveTask(taskBo);
                            }
                            try {
                                test1();

                            } catch (Throwable throwable) {
                                log.info("定时任务执行有误"+throwable);
                                taskBo.setOtherException(throwable.toString());
                                taskMapper.updateTask(taskBo);
                            }
                            try {
                                if(redisDistribute.releaseLock(name,isLock)) log.info("redis释放锁成功");
                            } catch (Exception e) {
                                log.error("redis异常"+e );
                                taskBo.setRedisException(e.toString());
                                taskBo.setStatus("5");
                                taskMapper.updateTask(taskBo);
                                log.info("<========使用mysql释放锁=======>");
                                try {
                                    if(sqlDistribute.releaseLock(name)){
                                        log.info("<========mysql释放锁成功=======>");
                                        taskBo.setStatus("8");
                                        taskMapper.updateTask(taskBo);
                                    }
                                } catch (Exception exception) {
                                    log.info("<========mysql释放锁失败联系管理员=======>");
                                    log.error("mysql异常"+e);
                                    taskBo.setStatus("6");
                                    taskBo.setMysqlException(exception.toString());
                                    taskMapper.updateTask(taskBo);
                                }
                            }
                        }
                    }

                    log.info(">>>>>>>>>>>"+Thread.currentThread().getName()+">>>>>>>>>>>>"+ ""+"<<<<<<<<<<<<<<<<");

                });
            countDownLatch.countDown();
        }
    }
    @Autowired
    private LockMapper lockMapper;
    @DistributedLock
    //@Scheduled(cron = "0 */1 *  * * * ")
    public void test1(){
        log.info("==========开始执行test方法===============");
        LockBo lockBo=lockMapper.findByLockName("distributedLock");
    }


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值