基于Redis【setnx】的分布式锁

不能用JDK级别的锁实现,因为这些锁只能保证单个JVM内线程安全,并不能保证分布式系统的线程安全

//待会离职版的分布式锁:没有办法防止锁失效,而且无法在redis集群上使用
@RestController
public class ProductController {

    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/test")
    public String lock(){
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("Lock",clientId,10,TimeUnit.SECONDS);//防止 kill -9

        if(!result){
            return "error";
        }
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock>0){
            RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();
            try{
                connection.multi();
                stringRedisTemplate.opsForValue().decrement("stock");
                connection.exec();
                System.out.println("库存:"+(stock-1));
            }catch (Exception e){
                connection.discard();
            }finally {
                if(clientId.equals(stringRedisTemplate.opsForValue().get("Lock")))
                stringRedisTemplate.delete("Lock");
            }
        }
        else {
            System.out.println("库存不足");
        }
        return "bill done";
    }

}

基于redisson的分布式锁【可以在单redis节点上解决锁失效的问题:其原理是设置另一个线程,每隔一段时间完成一次锁续命】

可以使用的基于redis的分布式锁,乐观锁:

@SpringBootApplication
public class RedisSpringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisSpringbootApplication.class, args);
    }

    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson)Redisson.create(config);
    }
}
//controller
@RequestMapping("/upgrade")
    public String lock2(){
        String lockKey = "lockKey2";
        RLock lock = redisson.getLock(lockKey);
        lock.lock();//其他线程自旋->乐观锁
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock>0){
            RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();
            try{
                connection.multi();
                stringRedisTemplate.opsForValue().decrement("stock");
                connection.exec();
                System.out.println("库存:"+(stock-1));
            }catch (Exception e){
                connection.discard();
            }finally {
               lock.unlock();
            }
            }
        return "bill done";
    }

redis集群的分布式锁解决方案

基于Zookeeper的解决方案可以解决主从失效问题

基于Redis&Redlock的解决方案【这个版本不完整,而且redlock模块不够完善】

String lockKey = "lockKey2";
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);

RedissonRedlock redlock	= new RedissonRedLock(lock1,lock2,lock3);

boolean res = redlock.trylock(waitTime,leasetime,TimeUnit.SECOND);//成功返回true
if(res){
    //业务逻辑
}
redlock.unlock();

基于zookeeper&curator的分布式锁

@Configuration
public class CuratorConf {

    String zookeeperConnectionString="222.201.144.247:2181";

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy);
        return client;
    }
}

注入即可使用

@Autowired
CuratorFramework curatorFramework;

InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/product_"+id);
if ( lock.acquire(maxWait, waitUnit) ) 
{
    try {  
        
    }catch{
    }
    finally
    {
        lock.release();
    }
}

分布式接口如何保证幂等性

需要在数据访问层Dao的增删改函数实现幂等性,防止网络延迟、用户误操作导致的幂等性问题

INSERT INTO city (provinceId,city_name,description) VALUES (2,"guangdong","none");

当我们使用插入语句时若指定主键,那么同样的语句二次执行的时候则会报错

对于删除语句:如果是绝对值删除,则没有幂等性问题,但是如果是相对值删除,则可能会出现幂等性问题,对于更新语句也是同理

对于前端而言:通过设置按钮只可以点击一次,防止用户重复点击发生的副作用

token机制:

产品上允许重复提交,具体实现是进入页面产生一个token,然后所有的请求都带上这个token,根据token来避免重复的请求

token+redis方案

提交订单时,申请一个和用户有关的token【全局唯一ID,redis写为幂等性】,存到redis缓存中,当发起支付请求,去redis中查看是否有对应的token,当支付完成,token被删除,这样就防止了用户的二次支付。

乐观锁方案

通过版本号更新数据,如果数据匹配则更新数据,如果版本号不匹配则不更新数据

全局唯一ID【雪花算法】

全局唯一、单调递增、信息安全【不连续】、含时间戳、高可用

方案一:使用jdk自带的UUID:

UUID.randomUUID().toString()//高性能、本地生成的UUID,然而其无序,插入mysql需要不断维护B+树索引,且无法保证全局唯一

方案二:基于redis集群:

高吞吐, 依赖incr、incrby实现原子性操作:例如数据库中有6条数据,设置初始值分别为1,2,3,4,5,6,每次随机取,取值结束后全部递增6【单调递增、信息安全、全局唯一、高可用,但是不含时间戳】

方案三:雪花算法:snowflake

最高位永远为0,用41位表示时间戳,用10bit表示进程数、12bit序列号位

springboot整合雪花算法
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.10</version>
</dependency>

uid-generator:

其使用的就是snowflake,只是在生产机器id,也叫做workId时有所不同。

uid-generator中的workId是由uid-generator自动生成的,并且考虑到了应用部署在docker上的情况,在uid-generator中用户可以自己去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配。说的简单一点就是:应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据,数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId,而数据由host,port组成。对于uid-generator中的workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,同一个应用每重启一次就会消费一个workId。

BASE理论

Basically Available(基本可用)

Soft state(软状态)

Eventually consistent(最终一致性)

Curator源码加锁

分布式事务

假设一次分布式事务由不同的操作组成,这些操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证原子性。本质上来说,分布式事务就是为了保证不同数据库的数据一致性

forword&redirect

forword是服务器内部的重定向,服务器直接访问目标地址的 url网址,把里面的东西读取出来,但是客户端并不知道,因此用forward的话,客户端浏览器的网址是不会发生变化的。

redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的地址。

token的生成

当用户第一次登入时,服务端验证用户密码并生成一串字符串,这个字符串将被作为token存储到HttpServletResponse,返回保存客户端的Cookie中,这样做的好处是保持登入状态无需在Cookie中存储用户名密码。当用户再次访问时,服务端通过HttpServletRequest.getCookies()查询用户请求中携带的cookie数据,通过和缓存中的token比较并验证有效期。

浏览器可以利用Cookie以键值对的方式存储token,比如token-name:token的形式。对于服务器端,可以利用缓存存储token与字符串形式的用户对象【转换推荐FASTjson,并且缓存具有自动失效的属性】,当用户请求通过负载均衡转发到某一节点时,该节点通过缓存的数据验证token的有效性,并获取到对应的用户对象。

 posted on 2022-03-12 13:06  春秋流千事  阅读(30)  评论(0)    收藏  举报