分布式事务锁-简易篇

当部署多个服务时,会同时存在对同一数据修改,从而造成脏数据或者没有按照规则、套路出牌的场景。为解决这个问题,redis的单线程进制可以解决这个问题,来保证事务依次执行。

1、首先,创建一个spring-boot项目。

2、然后,加入redis的maven依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3、在application.properties中加入redis配置。

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=500

4、编写redis锁工具类。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

/**
 * 分布式事务锁
 * @date 2020/03/31
 * @author zyh
 */
@Component
@Slf4j
public class RedisLockUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 执行加锁
     * @param key 实物id
     * @param value 当前时间+失效时间
     * @return
     */
    private Boolean lock(String key,String value){

        // 第一次加锁时
        // 调用redis的setnx命令,若成功,则说明是第一次加锁;若失败,则说明事务已存在
        if(redisTemplate.opsForValue().setIfAbsent(key, value)){
            return true;
        }

        // 若已存在事务,则接着往下验证
        // 接下来验证已存在的事务是否超时
        String currentVal=redisTemplate.opsForValue().get(key);
        Long currentTimeMillis=System.currentTimeMillis();
        Long currentValLong=Long.parseLong(currentVal);
        if(StringUtils.isNotEmpty(currentVal) && currentValLong.longValue()<currentTimeMillis.longValue()){
            // 事务超时时间 小于 当前时间,说明已超时
            // 重新设置新的时间
            String oldVal=redisTemplate.opsForValue().getAndSet(key, value);
            if(StringUtils.isNotEmpty(oldVal) && oldVal.equals(currentVal)){
                // 这一步判断是为了避免两个线程同时走到上一步,但是redis是单线程的,
                // 所以getset的返回值只有一个是和之前线程保持一致的
                // 所以,只有一个线程会成功。
                // 就是那个最先调用getset的
                return true;
            }
        }

        return false;
    }

    /**
     * 加锁-加入重试机制
     * @param key 实物id
     * @param value 当前时间+失效时间
     * @return
     */
    public Boolean doLock(String key,String value){

        Long currentTimeMillis=System.currentTimeMillis();
        Long valueLong=Long.parseLong(value);
        while(valueLong.longValue()>currentTimeMillis.longValue()){

            Boolean flag=lock(key,value);
            if(flag){
                return flag;
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            currentTimeMillis=System.currentTimeMillis();
        }

        return false;
    }

    /**
     * 删除锁
     * @param key 实物id
     * @param value 当前时间+失效时间
     */
    public void unLock(String key,String value){

        String currentVal=redisTemplate.opsForValue().get(key);
        if(StringUtils.isNotEmpty(currentVal) && currentVal.equals(value)){
            redisTemplate.opsForValue().getOperations().delete(key);
        }
    }

}

5、编写测试service。

import com.study.redislock.util.RedisLockUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@Service
public class UserService {

    @Autowired
    private RedisLockUtils redisLockUtils;

    private static final String USER_TABLE_NAME="user";

    private static final long REDIS_LOCK_TIME_OUT = 1000 * 60;

    private SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public void add(String id,String name){

        log.info("id:"+id+",name:"+name+",进来。。。。"+simpleDateFormat.format(new Date()));

        //加锁
        long time=System.currentTimeMillis()+REDIS_LOCK_TIME_OUT;
        Boolean lockFlag=redisLockUtils.doLock(USER_TABLE_NAME,String.valueOf(time));
        if(!lockFlag){
            log.error("id:"+id+",name:"+name);
            throw new RuntimeException("操作过于频繁,请稍后重试!"+id+","+name);
        }

        try {
            Thread.sleep(1000*30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("id:"+id+",name:"+name);

        //删除锁
        redisLockUtils.unLock(USER_TABLE_NAME,String.valueOf(time));

        log.info("id:"+id+",name:"+name+",结束。。。。"+simpleDateFormat.format(new Date()));
    }
}

6、编写测试用的controller。

import com.study.redislock.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/add")
    public Object add(
            @RequestBody Map<String,Object> map
            ){
        userService.add(String.valueOf(map.get("id")),String.valueOf(map.get("name")));

        return map;
    }
}

7、用postman开始测试。

8、查看控制台。

可见三个请求,差不多时间进来,但是他们的执行却是分别等前面的请求执行完。一个请求正常用时30ms,但是后两个请求大大超过了这个时间,说明分布式事务锁的作用。也可以采取debug进代码,看看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值