springboot分布式锁解决方案

数据库级别乐观锁锁解决方案:

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。
场景:更新库存:

# 根据商品id查询商品信息,判断商品是否存在及获取库存信息
select * from goods where id = 1; # good

# 库存一般可以为一件  也可以为n件 所以需要判断当前库存的值是否大于要减去库存的值
good.getStock()>='传入的要减去的库存值'?

# 带上版本号 更新库存,这里因为有竞态条件可能会导致几个线程都同时通过了第一层select查询,然后到达第二步,
# 准备更新信息,但是由于只要有一个更新成功了版本号就会加1,这样其他线程再修改就会发现版本号与数据库的
# 版本号不一致,导致更新失败,这样就在数据库层面采用乐观锁的方法避免了并发问题
UPDATE good SET stock = stock - #{stock,jdbcType=INTEGER},version = version + 1
    WHERE id = #{id,jdbcType=INTEGER} AND version=#{version,jdbcType=INTEGER} AND stock>0

乐观锁遵循的两点法则:

锁服务要有递增的版本号version
每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

缺点:
其实,借助更新时间戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前线获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间戳相等。

乐观锁的优点比较明显,由于在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。缺点是需要对表的设计增加额外的字段,增加了数据库的冗余,另外,当应用并发量高的时候,version值在频繁变化,则会导致大量请求失败,影响系统的可用性。我们通过上述sql语句还可以看到,数据库锁都是作用于同一行数据记录上,这就导致一个明显的缺点,在一些特殊场景,如大促、秒杀等活动开展的时候,大量的请求同时请求同一条记录的行锁,会对数据库产生很大的写压力。所以综合数据库乐观锁的优缺点,乐观锁比较适合并发量不高,并且写操作不频繁的场景。
乐观锁只能对一张表的数据进行加锁,如果是需要对多张表的数据操作加分布式锁,基于版本号的乐观锁是办不到的。

数据库级别悲观锁锁解决方案:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

优点:是比较安全的一种实现方法。
缺点:在高并发的场景下开销是不能容忍的。容易出现数据库死锁等情况。

在使用悲观锁的同时,我们需要注意一下锁的级别。MySQL InnoDB引起在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的数据),否则MySQL 将会执行表锁(将整个数据表单给锁住)。
在使用悲观锁时,我们必须关闭MySQL数据库的自动提交属性(参考下面的示例),因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)

实现


//0.开始事务

begin;/begin work;/start transaction; (三者选一就可以)

//1.查询出商品信息

select status from t_goods where id=1 for update;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

//4.提交事务

commit;/commit work;

可参考:悲观锁

使用redis实现分布式锁:

参考连接:redis实现分布式锁注意事项

代码参考:

final String key = String.format("redis:product:id:%s",productLockDto.getId()+"");
        boolean res = true;
        while (res) {
            String value = UUID.randomUUID().toString() + System.nanoTime();

            if (res) {//如果塞入成功  说明redis里面还没有这个key,可以获取锁进入
            //判断当前key是否已经存在于redis中
            res = stringRedisTemplate.opsForValue().setIfAbsent(key, value);

                try {
                    ProductLock productLock = productLockMapper.selectByPrimaryKey(productLockDto.getId());
                    //数据库的库存必须大于等于  请求所需要减去的库存
                    if (productLock != null && productLock.getStock().compareTo(productLock.getStock()) >= 0) {
                        productLock.setStock(productLockDto.getStock());
                        result = productLockMapper.updateStockForNegative(productLock);
                        if (result > 0) {
                            log.info("这个成功的更新了:{}", productLockDto.getStock());
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //不管插入成功与否,都要释放锁,不然id对应的key一直在redis中,相当于释放锁
                    //释放锁的前提是一定是加锁和释放锁是同一个锁,可以采用判断key对应的值是不是相等来判断是不是同一把锁

                    String redisValue = stringRedisTemplate.opsForValue().get(key);
                    if (!Strings.isNullOrEmpty(redisValue) && value.equals(redisValue)) {
                        log.info("删除:");
                        stringRedisTemplate.delete(key);
                    }
                }
            } else {
                //res = true;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
       

使用zookeeper/redisson实现分布式锁:

代码参考:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>com.debug.steadyjack</groupId>
		<artifactId>parent</artifactId>
		<version>1.0.1</version>
	</parent>

	<artifactId>server</artifactId>
	<name>server</name>
	<packaging>jar</packaging>

	<properties>
		<spring-boot.version>1.3.3.RELEASE</spring-boot.version>

		<slf4j.version>1.7.13</slf4j.version>
		<log4j.version>1.2.17</log4j.version>
		<mysql.version>5.1.37</mysql.version>
		<druid.version>1.0.16</druid.version>
		<guava.version>19.0</guava.version>
		<joda-time.version>2.9.2</joda-time.version>
		<poi.version>3.15</poi.version>
		<weixin-java-cp.version>2.5.1</weixin-java-cp.version>
		<elastic-job.version>2.1.4</elastic-job.version>
		<retrofit.version>2.3.0</retrofit.version>

		<cglib.version>3.1</cglib.version>
		<dubbo.version>2.8.4</dubbo.version>
		<resteasy.version>3.0.14.Final</resteasy.version>
		<disconf.version>2.6.36</disconf.version>

		<commons-fileupload.version>1.3.1</commons-fileupload.version>
		<curator.version>2.10.0</curator.version>
		<zookeeper.version>3.4.6</zookeeper.version>
		<redisson.version>3.8.2</redisson.version>
	</properties>

	<!-- 依赖管理 -->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>${spring-boot.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>


	<dependencies>

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


		<!--依赖平级模块-->
		<dependency>
			<groupId>com.debug.steadyjack</groupId>
			<artifactId>model</artifactId>
			<version>${project.parent.version}</version>
		</dependency>

		<!--guava-->
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>${guava.version}</version>
		</dependency>

		<!--mysql-->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql.version}</version>
		</dependency>

		<!--druid-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>${druid.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>${spring-boot.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>


		<!--热部署-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope><!-- zookeeper start -->
			<optional>true</optional>
		</dependency>


		<dependency>
			<groupId>org.apache.zookeeper</groupId>
			<artifactId>zookeeper</artifactId>
			<version>${zookeeper.version}</version>
			<exclusions>
				<exclusion>
					<artifactId>slf4j-log4j12</artifactId>
					<groupId>org.slf4j</groupId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.apache.curator</groupId>
			<artifactId>curator-framework</artifactId>
			<version>${curator.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.curator</groupId>
			<artifactId>curator-recipes</artifactId>
			<version>${curator.version}</version>
		</dependency>

		<!-- zookeeper end -->

		<!--redisson-->
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>${redisson.version}</version>
		</dependency>


	</dependencies>

	<build>
		<finalName>server_${project.parent.version}</finalName>

		<!--spring boot打包插件-->
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>

		<!--资源目录配置-->
		<resources>
			<resource>
				<directory>src/main/resources</directory>
				<filtering>true</filtering>
			</resource>
		</resources>

	</build>
</project>

#zookeeper配置 zk.host=127.0.0.1:2181 zk.namespace=spring_boot_distribute

//redisson
	@Bean
	public RedissonClient redissonClient(){
		Config config = new Config();
		config.useSingleServer().setAddress(env.getProperty("redisson.address"));
		RedissonClient redissonClient = Redisson.create(config);
		return redissonClient;
	}


	//zookeeper客户端框架
	@Bean
	public CuratorFramework curatorFramework(){
		CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(env.getProperty("zk.host")).namespace(env.getProperty("zk.namespace"))
				.retryPolicy(new RetryNTimes(5,1000)).build();

		curatorFramework.start();
		return curatorFramework;
	}

	public static void main(String[] args) {
		SpringApplication.run(ServerApplication.class, args);
	}
@Service
@Slf4j
public class DataLockService2Impl implements DataLockService2 {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private ProductLockMapper productLockMapper;

    private final static String countKey="redis:lock:count";

    @Autowired
    private CuratorFramework client;

    private static final String pathPrefix="/springboot/zkLock/";

    @Autowired
    private RedissonLock redissonLock;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateStock(ProductLockDto productLockDto) {
        int count = 0;
        //这里不用校验dto  因为controller已经校验过了
        ProductLock pd = productLockMapper.selectByPrimaryKey(productLockDto.getId());
        if(pd!=null && pd.getStock().compareTo(productLockDto.getStock())>=0){
            pd.setStock(productLockDto.getStock());
            count = productLockMapper.updateStock(pd);
        }
        return count;
    }

    @Override
    public int updateStockV1(ProductLockDto productLockDto) {
        int count = 0;
        //这里不用校验dto  因为controller已经校验过了
        ProductLock pd = productLockMapper.selectByPrimaryKey(productLockDto.getId());
        if(pd!=null && pd.getStock().compareTo(productLockDto.getStock())>=0){
            pd.setStock(productLockDto.getStock());
            count = productLockMapper.updateStockV1(pd);
        }
        return count;
    }

    @Override
    public int updateStockV2(ProductLockDto productLockDto) {//悲观锁实现
        int count = 0;
        //这里不用校验dto  因为controller已经校验过了
        ProductLock pd = productLockMapper.selectByPKForNegative(productLockDto.getId());
        if(pd!=null && pd.getStock().compareTo(productLockDto.getStock())>=0){
            pd.setStock(productLockDto.getStock());
            count = productLockMapper.updateStockForNegative(pd);
        }
        return count;
    }

    @Override
    public int updateStockV3(ProductLockDto productLockDto) {
        //使用redis 的setnx加锁
        int result = 0;


        //TODO:只有当当前key不存在的时候,SETNX 会成功 – 此时相当于获取到可以对这个资源进行操作的同步锁
        final String key=String.format("redis:product:id:%s",productLockDto.getId()+"");

        Boolean res=true;
        while(res){
            String value= UUID.randomUUID().toString()+System.nanoTime();
            res=stringRedisTemplate.opsForValue().setIfAbsent(key,value);

            if (res){
                stringRedisTemplate.opsForValue().increment(countKey,1L);

                try {
                    //TODO:执行真正的处理逻辑
                    res=false;

                    ProductLock entity=productLockMapper.selectByPrimaryKey(productLockDto.getId());
                    if (entity!=null && entity.getStock().compareTo(productLockDto.getStock())>=0){
                        entity.setStock(productLockDto.getStock());
                        result=productLockMapper.updateStockForNegative(entity);

                        if (result>0){
                            log.info("基于redis实战分布式锁-成功:stock={} ",productLockDto.getStock());
                        }else{
                            //TODO:发送一条异步信息,记录客户的手机号,等待下一次搞活动通知!
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    //TODO:释放锁-释放当时自己获取到的锁-value
                    String redisValue=stringRedisTemplate.opsForValue().get(key);
                    if (!Strings.isNullOrEmpty(redisValue) && redisValue.equals(value)){
                        log.info("删除redis key");
                        stringRedisTemplate.delete(key);
                    }
                }
            }else{
                res=true;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        return result;
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateStockV4(ProductLockDto dto) throws Exception {
        int res = 0;
        InterProcessMutex processMutex = new InterProcessMutex(client, pathPrefix + dto.getId() + "-lock");
        try {

            if(processMutex.acquire(10, TimeUnit.SECONDS)){
                ProductLock productLock = productLockMapper.selectByPrimaryKey(dto.getId());
                if(productLock!=null && productLock.getStock()>=dto.getStock()){
                    productLock.setStock(dto.getStock());
                    res=productLockMapper.updateStockForNegative(productLock);

                    if (res>0){
                        log.info("基于zookeepr实战分布式锁-成功:stock={} ",dto.getStock());
                    }

                }
            }else{
                throw new RuntimeException("获取zk分布式锁失败!");
            }
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            processMutex.release();
        }
        return 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateStockV5(ProductLockDto dto) throws Exception {
        int res = 0;
        RLock rLock = redissonLock.acquireLock(String.valueOf(dto.getId()));
        try {

            if(rLock!=null){
                ProductLock productLock = productLockMapper.selectByPrimaryKey(dto.getId());
                if(productLock!=null && productLock.getStock()>=dto.getStock()){
                    productLock.setStock(dto.getStock());
                    res=productLockMapper.updateStockForNegative(productLock);

                    if (res>0){
                        log.info("基于Redisson实战分布式锁-成功:stock={} ",dto.getStock());
                    }

                }
            }else{
                throw new RuntimeException("获取redisson分布式锁失败!");
            }
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            if(rLock!=null){
                redissonLock.realeaseLock(rLock);
            }
        }
        return res;
    }
}
@Slf4j
@Component
public class RedissonLock implements InitializingBean {


    @Autowired
    private RedissonClient redissonClient;

    private static Redisson redisson;

    @Override
    public void afterPropertiesSet() throws Exception {
        redisson = (Redisson) redissonClient;
    }


    public RLock acquireLock(String lockName) {
        RLock lock = redisson.getLock(lockName);
        lock.lock(); //v1

        //lock.lock(10L, TimeUnit.SECONDS); //v2
        //lock.tryLock(110L,10L,TimeUnit.SECONDS);//+v3
        return lock;
    }

    public void realeaseLock(RLock lock) {
        lock.unlock();
    }

    /**
     * 塞入分布式set对象中
     *
     * @param key
     * @param value
     */

    public RSet setKeyValue(final String key, final String value) {
        if (!Strings.isNullOrEmpty(key)) {
            RSet<String> rSet = redisson.getSet(key);
            rSet.add(value);
            //rSet.expire(24L, TimeUnit.HOURS); //可以设置过期时间
            return rSet;
        }
        return null;
    }


    /**
     * key是否存于分布式set对象中
     *
     * @param key
     * @return
     */

    public Boolean existKey(final String key) {
        Boolean result = false;
        RSet<String> rSet = redisson.getSet(key);
        if (rSet != null && rSet.size() > 0) {
            result = true;
        }
        return result;
    }

}

控制并发与控制是否重复示例:

@Service
public class UserService implements IUserService {

    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedissonLock redisLock;

    private static final String lockKeyPrefix = "redisson:userName:";

    @Autowired
    private CuratorFramework client;

    private static final String zkPrefix = "/repeat/submit/";

    private static final String zkRedisKeyPrefix = "zkRedis:repeat:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 注册
     *
     * @param userDto
     * @return
     */

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int register(UserDto userDto) throws Exception {
//        int result=0;
//        User user=new User();
//        BeanUtils.copyProperties(userDto,user);
//        user.setCreateTime(new Date());
//        userMapper.insertSelective(user);
//        return result; //v1.0

        //v2.0
//        int result=0;
//
//        //TODO:思路-(1)为共享资源加锁;(2)为共享资源在某个缓存之地 如rset 加入存在的标识-就是将共享资源加入就好了!!
//
//        RLock rLock=redisLock.acquireLock(userDto.getUserName());
//        try {
//            String key=lockKeyPrefix+userDto.getUserName();
//            if (!redisLock.existKey(key)){
//                redisLock.setKeyValue(key,userDto.getUserName());
//
//                User user=new User();
//                BeanUtils.copyProperties(userDto,user);
//                user.setCreateTime(new Date());
//                userMapper.insertSelective(user);
//            }else{
//                log.info("已存在于redisson的set存储中:{}",key);
//            }
//
//        }catch (Exception e){
//            e.printStackTrace();
//            throw e;
//        }finally {
//            redisLock.realeaseLock(rLock);
//        }
//
//        return result;


        //v3.0
        int result = 0;

        InterProcessMutex mutex = new InterProcessMutex(client, zkPrefix + userDto.getUserName() + "-lock");
        try {
            if (mutex.acquire(10L, TimeUnit.SECONDS)) {

                final String realKey = zkRedisKeyPrefix + userDto.getUserName();
                if (!stringRedisTemplate.hasKey(realKey)) {
                    stringRedisTemplate.opsForValue().set(realKey, UUID.randomUUID().toString());

                    User user = new User();
                    BeanUtils.copyProperties(userDto, user);
                    user.setCreateTime(new Date());
                    userMapper.insertSelective(user);
                } else {
                    log.info("已存在于redis的key中:{}", realKey);
                }

            } else {
                throw new RuntimeException("获取zk分布式锁失败!");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            mutex.release();
        }

        return result;

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值