分布式锁的解决方案

超卖:商品卖出数量超出库存数量
原因:用户同时扣减库存

解决方法1:

1.扣减库存不在程序中进行,而是通过数据库
2.向数据库传递库存增量,扣减1个库存,增量为-1
3.在数据库update语句计算库存,通过update行锁解决并发

  <update id="updateByPrimaryKeySelective">
        update product
        set count = count - #{purchaseProductNum},
            create_user=#{userName}
        where id = #{id}
  </update>

存在的问题:
并发检验库存,update更新库存,导致库存为负数;

解决方法:
(校验库存和扣减库存),统一加锁,使之成为原子性操作;

1.用Synchronized锁

注意:
Synchronized和@Transactional同时使用的问题:

@Transactional(rollbackFor=Exception.class)
public synchronized String update(String req)throws Exception {
    
}

因为 synchronized方法在事务里面,当多个线程进入方法时,可能事务还未commit提交,导致读取的数据是更新前的;
解决办法:手动执行事务,这样可以保证事务在synchronized方法内部,提交事务之后,再释放锁;

    @Autowired
    private PlatformTransactionManager platformTransactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;
    public synchronized String update(String req)throws Exception {
     //获取事务
	 TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
	 ...
	 //异常,回滚事务
	 platformTransactionManager.rollback(transaction);
	 ...
	 //执行完数据库更新操作后,提交事务
	 platformTransactionManager.commit(transaction)}

也可以使用synchronized块

synchronized(this){

}
1.用ReentrantLock锁

注:抛异常的时候,记得释放锁,可以放在finally中

private Lock lock = new ReentrantLock();
public  String update(String req)throws Exception {
	lock.lock();
	try{
		...
	}finally{
		lock.unlock();
	}
}

基于数据库实现分布式锁

1.多个进程,多个线程访问共同组件数据库
2.通过select…for update访问同一条数据
3.for update锁定数据,其它线程只能等待

business_code字段用来区分不同业务的锁

DistriLock distriLock = distriLockMapper.selectByBusinessCode("demo");
    select 
    <include refid="Base_Column_List" />
    from distribute_lock
    where business_code = #{businessCode,jdbcType=VARCHAR}
    for update

优缺点:
1.优点:简单方便,易于理解,易于操作;
2.缺点:并发量大时,对数据库压力较大;
建议:作为锁的数据库和业务数据库分开;

基于Redis的Setnx实现分布式锁

1.获取锁的Redis命令:

SET resource_name my_random_value NX PX 30000

(1).resource_name 资源名称,可以根据不同的业务区分不同的锁
(2).my_random_value 随机值,每个线程的随机值都不同,用于释放锁时的校验
(3).NX key不存在时设置成功,key存在则设置不成功
(4).PX 自动失效时间,出现异常情况,锁可以过期失效
(5).释放锁采用Redis的delete命令
(6).释放锁时,要校验随机值my_random_value,相同才释放,用LUA脚本实现
参照Redis官网

# 获取设置的my_random_value,和传入的ARGV[1]做比较,如果相同,则执行删除操作,否则不操作;
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0
end

为什么释放锁的时候,要校验随机值my_random_value?

比如有A/B两个线程,A获得锁------>A执行任务, 异常情况:执行任务时间过长,导致任务没结束,锁已经过期释放;
这时,B获得锁------>B执行任务,如果不校验A线程设置锁时候的随机值,则这时A会把B设置的锁给删除;

代码实现

可以封装一下,那样不用每个业务都写一遍;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/redisLock")
    public String redisLock(){
        System.out.println("进入方法");
        String key = "redisKey";
        String value = UUID.randomUUID().toString();
        RedisCallback<Boolean>  redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(30);
            //序列化key
            byte[] reidsKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            //执行setnx操作
            Boolean result = connection.set(reidsKey, redisValue, expiration, setOption);
            return result;
        };
        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        if(lock){
            System.out.println("获取了锁");
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                        "   return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "   return 0\n" +
                        "end";
                RedisScript<Boolean> redisScript = RedisScript.of(luaScript, Boolean.class);
                List<String> keys = Arrays.asList(key);
                Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
                System.out.println("释放锁的结果" + result);
            }
        }
        return "方法执行完成";
    }
基于分布式锁解决定时任务重复的问题

定时任务集群部署,任务重复执行?
使用分布式锁解决;

基于Zookeeper的临时节点实现分布式

Zookeeper数据结构:
1.持久节点
2.瞬时节点,有序,瞬时节点不可再有子节点,会话结束后,瞬时节点自动消失

Zookeeper的观察器

1.可设置观察器的3个方法:getData(); getChildren(); exists();
2.节点数据变化,发送给客户端
3.观察器只能监控一次,再监控需重新设置

实现原理

1.利用Zookeeper的瞬时有序节点的特性
2.多线程并发创建瞬时节点,得到有序的序列
3.序号最小的线程获得锁
4.其它的线程则监听自己序号的前一个序号
5.前一个线程执行完成,删除自己序号的节点
6.下一个序号的线程得到通知,继续执行
7.创建节点时,已经确定了线程执行的顺序
在这里插入图片描述

代码实现:

public class ZkLock implements AutoCloseable, Watcher {

    private ZooKeeper zooKeeper;
    private String znode;

    public ZkLock() throws IOException {
        zooKeeper = new ZooKeeper("192.168.1.105:2181",10000,this);
    }

    public boolean getLock(String businessCode){
        try {
            //创建业务根节点
            Stat stat = zooKeeper.exists("/" + businessCode, false);
            if(stat == null){
                zooKeeper.create("/"+businessCode,businessCode.getBytes(),
                                     ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                     CreateMode.PERSISTENT);
            }
            //创建瞬时有序节点 比如:/order/order_0000000001
            znode = zooKeeper.create("/"+businessCode+"/"+businessCode+"_",
                                     businessCode.getBytes(),
                                     ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                     CreateMode.EPHEMERAL_SEQUENTIAL);
            //获取业务节点下所有的子节点
            List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
            //子节点排序
            Collections.sort(childrenNodes);
            //获取序号最小的(第一个)子节点
            String firstNode = childrenNodes.get(0);
            //如果创建的节点是第一个子节点,则获得锁
            if(znode.endsWith(firstNode)){
                return true;
            }
            //不是第一个子节点,则监听前一个节点
            String lastNode = firstNode;
            for (String node : childrenNodes){
                if(znode.endsWith(node)){
                    zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
                    break;
                }else{
                    lastNode = node;
                }
            }
            synchronized (this){
                wait();
            }
            return  true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public void close() throws Exception {
        zooKeeper.delete(znode,-1);
        zooKeeper.close();
        System.out.println("我已经释放了锁");
    }
    
    @Override
    public void process(WatchedEvent event) {
        if(event.getType() == Event.EventType.NodeDeleted){
            synchronized (this){
                notify();
            }
        }
    }
}
@RestController
public class ZkController {

    @RequestMapping("/zkLock")
    public String zkLock(){
        System.out.println("进入方法");
        try(ZkLock zkLock = new ZkLock()){
            if(zkLock.getLock("order")){
                System.out.println("获取了锁");
                Thread.sleep(10000);
            }else{
                System.out.println("没有抢到锁");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "方法执行完成";
    }
}
基于Zookeeper的Curator客户端实现分布式锁

1.引入curator客户端
2.curator已经实现了分布式锁方法
3.直接调用即可

代码实现:
curator官网:
https://curator.apache.org/getting-started.html
注意点:curator和zookeeper版本的兼容性

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>
@RestController
public class CuratorController {

    @Autowired
    private CuratorFramework client;

    @RequestMapping("/zkLock")
    public String zkLock() {
        System.out.println("进入方法");
        InterProcessMutex lock = new InterProcessMutex(client, "/book");
        try {
            if (lock.acquire(30, TimeUnit.SECONDS)) {
                System.out.println("获取了锁");
                Thread.sleep(10000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "任务完成";
    }
}
@MapperScan(basePackages = "cn.migu.deal.mapper")
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) throws Exception {
          SpringApplication.run(DemoApplication.class,args);
    }

    @Bean(initMethod = "start",destroyMethod = "close")
    public CuratorFramework getCuratorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.1.105:2181", retryPolicy);
        return client;
    }
}

基于Redisson实现分布式锁

1.引入Redisson的jar包
2.进行Redisson与Redis的配置
3.使用分布式锁
4.通过JAVA API方式引入Redisson
5.Spring项目引入Redisson
6.Spring Boot项目引入Redisson

代码实现:

@RestController
public class RedissonController {

    @RequestMapping("/redissonLock")
    public String redissonLock(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.1.105:6379");
        RedissonClient redisson = Redisson.create(config);
        RLock rLock = redisson.getLock("order");
        System.out.println("进入方法");
        try {
            rLock.lock(30, TimeUnit.SECONDS);
            System.out.println("获得锁");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("释放锁");
            rLock.unlock();
        }
        return "方法执行完成";
    }
}
Springboot集成redisson:

https://blog.csdn.net/lilinn01/article/details/108282446

Spring集成redisson:

https://blog.csdn.net/wang_keng/article/details/73549274

分布式锁选型

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值