Redis - Coupon spike, inventory oversold, distributed lock, Redisson

Article Directory

1. Coupon flash sale

1.1 Overview of Globally Unique ID

Coupons can be issued for each store

image-20230628092618341

When the user rushes to buy, the order will be generated and saved in the tb_voucher_order table, and there are some problems if the order table uses the database auto-increment ID

  • id regularity is too obvious

    Users can guess some information based on the id.

    For example, I placed an order for a product yesterday with an id of 10, and today I placed an order for a product with an id of 100, then I can guess that 90 products were sold during this period

  • Limited by the amount of data in a single table

    As long as the user has an order behavior, new orders will continue to be generated.

When the website reaches a certain scale, there will be tens of millions of orders a year, and a single table will not be able to store such large-scale data after a few years.

We can only divide it into a single table for storage, and each table adopts the method of id auto-increment, which obviously cannot be realized, because each mysql table calculates its own self-increment, and it all starts from "1". Some data ids in the table may be repeated

If the id is repeated, there will be problems in querying the order in the future

To solve the above problem, we can use the global ID generator

The global ID generator is a tool used to generate a globally unique ID in a distributed system

Global meaning: Under the same business, no matter how many services, how many nodes, and how many different tables are divided into the distributed system in the future, as long as the id obtained by using the global ID generator must be unique in the current business id, there will be no conflict

Of course, it doesn’t matter if the ids of different businesses conflict

Monolithic projects also use the global ID generator

Generally, the following characteristics must be met

  • uniqueness

  • high availability

    At least use it and you can't hang it up

  • high performance

    Generate id correctly, and generate id faster

  • increment new

    To have a monotonically increasing property. Although it is not self-increasing like a database, we need to ensure that the overall id is a process of gradually increasing, which is conducive to creating indexes for the database

  • safety

    Regularity should not be too obvious


There is an incr command in our Redis command

Redis command - general command, String type, Key hierarchy, Hash type, List type, Set type, SortedSet type_redis multi-level command query_I Love Brown Bear's Blog-CSDN Blog

First of all, uniqueness can be ensured, because Redis is independent of the database, no matter how many tables or databases the database has, there is only one Redis, so when everyone visits Redis, its self-increment must be unique

High availability, high performance, and incrementality are not explained

Security is a bit more troublesome, the key depends on what self-increment we use , if it is still 1, 2, 3, 4, ... then the security will definitely be poor

In order to ensure security, we can stitch some other information

Considering the performance of the database, our id still uses the numeric type (Long)

image-20230628095913369

The first digit is always 0, which means our sign is always a positive number

Putting a 31-bit timestamp in the middle part actually increases the complexity of the id, why is it 31 bits, because we want to use seconds as the unit (we need to define an initial time as a benchmark, such as 2000, these 31 bits represent seconds , just calculate the time difference from 2000, 31 bits can represent up to 69 years)

The last part places the serial number, which is the auto-increment value of the Redis command incr

In this way of planning, it can support 2 to the 32 power orders in one second



Globally unique ID generation strategy

  • UUID

    Use JDK's built-in tool class to generate a long string of hexadecimal values. Because it is hexadecimal, it returns a string structure, and there is no monotonically increasing feature.

  • Redis self-increment

    has been analyzed above

  • snowflake snowflake algorithm

    It is also a 64-bit number of long type, and the self-increment adopts the self-increment of the current machine, which is internally maintained, so a mechanical id must be maintained

  • database self-increment

Redis self-increment ID strategy

  • One key per day, convenient to count orders
  • Limit the auto-increment value so that it does not exceed the upper limit
  • ID construction is timestamp + counter

1.2 Redis implements globally unique ID

When learning Redis global ID self-increment by bit operation, why use OR operation when returning to complete this operation-Big Data-CSDN Q&A

Implementation code

@Component
public class RedisIdWorker {
    
    
//  2022年1月1日0时0分0秒  的 秒数
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * id生成策略
     *
     * @param keyPrefix 业务前缀
     * @return 生成的id
     */
    public long nextId(String keyPrefix) {
    
    
//      TODO 1.生成时间戳(当前时间减去我们规定的开始时间的秒数)
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond  - BEGIN_TIMESTAMP;

//      TODO 2.生成序列号
//      同一个业务不要使用同一个key,因为incr自增有限度,2的64次方
//      为了预防订单数日积月累超过2的64次方,我们可以再拼接一个日期字符串,这样做还能方便以后统计
//      TODO 2.1 获取当前日期
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));

//      TODO 2.2 自增长
//      返回值是Long,但是我们改成了long,改完出现警告:会有空指针问题
//      但是并不会出现空指针问题,加入此key不存在,它会在自动帮我们创建一个key
        long count = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + ":" + date);

//      TODO 拼接两部分
//      我们的返回值是long,直接拼接timestamp与count就是字符串了,不能直接拼接
//      为了解决这个问题,我们使用位运算
//      timestamp<<32 时间戳向左移动32位,把redis自增的数创建出来,空出来的数以0位补充
//      | 代表或运算,一个为真,就是真 0|0=0, 0|1=1,因为后面32位都是0,所以还是count本身
        return timestamp<<32 | count ;
    }

//    public static void main(String[] args) {
    
    

//        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
//        传入一个时区作为参数
//        long second = time.toEpochSecond(ZoneOffset.UTC);
//        System.out.println(second);
//    }

}

Test below

Test in the test class. If it keeps turning around, close the Redis client and reconnect it.

@Resource
private RedisIdWorker redisIdWorker;

private ExecutorService es = Executors.newFixedThreadPool(500);

@Test
void testIdWorker() throws InterruptedException {
    
    
    CountDownLatch latch = new CountDownLatch(300);

    Runnable task = () -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();
    };
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 300; i++) {
    
    
        es.submit(task);
    }
    System.out.println("laizhelile");
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}

image-20230628113040239

image-20230628113249355

1.3 Add Coupon

Cheap and special coupons. Parity coupons can be purchased at will, while special coupons need to be snapped up

image-20230628135424992

  • tb_voucher : Basic information of cheap coupons, coupon amount, usage rules

    There is no stock in the following list, anyone can buy it at will

image-20230628135947536

  • tb_seckill_voucher : Special coupon inventory, start time of buying, end time of buying

image-20230628140045841

1.3.1 entity

Voucher class, which also contains the key information of flash coupons, inventory, start time, end time

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher")
public class Voucher implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商铺id
     */
    private Long shopId;

    /**
     * 代金券标题
     */
    private String title;

    /**
     * 副标题
     */
    private String subTitle;

    /**
     * 使用规则
     */
    private String rules;

    /**
     * 支付金额
     */
    private Long payValue;

    /**
     * 抵扣金额
     */
    private Long actualValue;

    /**
     * 优惠券类型
     */
    private Integer type;

    /**
     * 优惠券类型
     */
    private Integer status;
    /**
     * 库存
     */
    @TableField(exist = false)
    private Integer stock;

    /**
     * 生效时间
     */
    @TableField(exist = false)
    private LocalDateTime beginTime;

    /**
     * 失效时间
     */
    @TableField(exist = false)
    private LocalDateTime endTime;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

SeckillVoucher class, seckill coupon class, the purpose is to supplement the Voucher class

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_seckill_voucher")
public class SeckillVoucher implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 关联的优惠券的id
     */
    @TableId(value = "voucher_id", type = IdType.INPUT)
    private Long voucherId;

    /**
     * 库存
     */
    private Integer stock;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 生效时间
     */
    private LocalDateTime beginTime;

    /**
     * 失效时间
     */
    private LocalDateTime endTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

1.3.2 Controller

/**
 * 新增秒杀券
 * @param voucher 优惠券信息,包含秒杀信息
 * @return 优惠券id
 */
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
    
    
    voucherService.addSeckillVoucher(voucher);
    return Result.ok(voucher.getId());
}

Notice! We use Voucher to receive the information of the entity class to be added, including seckill information

1.3.3 Service layer

@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    
    
    // 保存优惠券
    save(voucher);
    
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
}

1.3.4 Testing

Request address: http://localhost:8081/voucher/seckill,

Add request header: authorization, the specific value can be copied on the page

{
    
    
    "shopId": 1,
    "title": "100元代金券",
    "subTitle": "周一至周五均可使用",
    "rules": "全场通用\n无需预约\n可无限叠加\n不兑现、不找零\n仅限堂食",
    "payValue": 8000,
    "actualValue": 10000,
    "type": 1,
    "stock": 10,
    "beginTime": "2022-01-25T10:09:17",
    "endTime": "2025-01-26T12:09:04"
}

image-20230628143151341

Look at the display effect of the page again

image-20230628144040904

1.4 Place an order with a coupon

Involved table tb_voucher_order order table

Currently the most important thing for us is user id, voucher id

image-20230628144757871

Case : Two points need to be judged when placing an order

  • Whether the flash sale has started or ended, if it has not started or has ended, you cannot place an order
  • Whether the inventory is sufficient, if not enough, the order cannot be placed

image-20230628150115919

1.4.1 entity

Coupon order entity class

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher_order")
public class VoucherOrder implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.INPUT)
    private Long id;

    /**
     * 下单的用户id
     */
    private Long userId;

    /**
     * 购买的代金券id
     */
    private Long voucherId;

    /**
     * 支付方式 1:余额支付;2:支付宝;3:微信
     */
    private Integer payType;

    /**
     * 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款
     */
    private Integer status;

    /**
     * 下单时间
     */
    private LocalDateTime createTime;

    /**
     * 支付时间
     */
    private LocalDateTime payTime;

    /**
     * 核销时间
     */
    private LocalDateTime useTime;

    /**
     * 退款时间
     */
    private LocalDateTime refundTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

1.4.2 Controller

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    
    

    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
    
    
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

1.4.3 Service

The id of the coupon is the same as the id of the seckill coupon

  @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
    
    
//      TODO 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        
//      TODO 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动尚未开始");
        }
        
//      TODO 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动已经结束");
        }

//      TODO 4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }
        
//      TODO 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .update();
        if (!success) {
    
    
            return Result.fail("扣减失败,可能库存不足");
        }
        
//      TODO 6.创建订单
//      我们只管订单id,代金券id,下单用户id
        VoucherOrder voucherOrder = new VoucherOrder();
        
//      TODO 6.1 订单id
//      使用自定义id生成器生成id
        long orderID  = redisIdWorker.nextId("order");
        voucherOrder.setId(orderID);
        
//      TODO 6.2 用户id
//      我们之前编写的登录拦截器
        UserDTO user = UserHolder.getUser();
        voucherOrder.setUserId(user.getId());
        
//      TODO 6.3 代金券id
        voucherOrder.setVoucherId(voucherId);

        save(voucherOrder);
        
//      TODO 7.返回订单id
        return Result.ok(orderID);
    }

You can take a look at this article, there is an explanation of the UserHolder login interceptor

SMS login based on Session_c# SMS verification code login_I love Brown Bear's Blog-CSDN Blog

1.4.3 Testing

Data appears in the order table

image-20230628153853800

And the number of flash coupons is also reduced by 1

image-20230628154107655

1.5 Inventory oversold problem

In the real seckill scenario, there are countless users rushing to buy together, click the button, and the number of concurrency per second may be hundreds or even thousands

We use Jmeter to test the program just now, create 200 threads to snap up coupons, but the number of coupons in the database is 100, it stands to reason that 100 people should not be able to grab them

But looking at the final result graph, I found that 45% of the people failed, which is supposed to be 50%.

image-20230628154619135

Look at the amount of the database, the inventory is -9

image-20230628154703556

In the high-concurrency scenario, the inventory oversold problem occurs. This is a problem that is easy to occur in the spike scenario, and it is also an unacceptable problem.

1.5.1 Reasons for oversold inventory

The high concurrency scenario remains the same, assuming we still have 1 item in stock

Thread 2 inquires again after thread 1 inquires, and finds that there is still inventory

This is only two threads, if there are more threads, it will be more troublesome, and the deduction will be more

image-20230628155231633

The overselling problem is a typical multi-thread security problem. A common solution to this problem is to add locks

Pessimistic locks and optimistic locks are not real locks, but a concept of lock design

Pessimistic locking affects efficiency, and it is not easy to use under the conditions of multi-threaded concurrency.

Moreover, we only have multi-threaded concurrency problems in rare cases, and it is not very good to use pessimistic locks

Optimistic locking is quite optimistic, so we use optimistic locking

image-20230628163847818

1.5.2 Introducing optimistic locking

The key to optimistic locking is to judge whether the data obtained by the previous query has been modified. There are two common ways:

  • version number issued

    The most widely used and universal.

    Add a version version to the field. Whenever the data is modified, the version number will be +1. If you want to determine whether the data has been modified, just compare the version number.

    image-20230628170326566

image-20230628170308385

  • CAS method

In fact, some optimizations have been made to the version number method. The version number is actually used to mark whether the data has been modified.

In fact, we can use inventory instead of version, and directly compare whether the inventory quantity has changed when updating

image-20230628170756367

1.5.3 Optimistic lock to solve oversold inventory

We use the CAS method to modify the previous business logic

Only need to modify the fifth step and add a new condition

//      TODO 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1") //set stock = stock-1
                .eq("voucher_id", voucherId) //where  voucher_id= voucherId
                .eq("stock",voucher.getStock())//where  stock= stock
                .update();
        if (!success) {
    
    
            return Result.fail("扣减失败,可能库存不足");
        }

After the modification, it was found that the abnormal rate was as high as 89%, and the number of people who failed greatly increased. It is better not to change it.

image-20230628171453437

Observe the database and find that the inventory is not sold more

image-20230628171601497

Why is it so wrong if it is not sold out?

It involves a disadvantage of optimistic locking, the success rate is too low, as shown in the figure below

image-20230628171745219

Improvements to optimistic locking

It is very easy to change the inventory, as long as the inventory is greater than 0

//      TODO 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1") //set stock = stock-1
                .eq("voucher_id", voucherId) //where  voucher_id= voucherId
                .gt("stock",0)//where  stock>0
                .update();
        if (!success) {
    
    
            return Result.fail("扣减失败,可能库存不足");
        }

Perfectly met our expectations

image-20230628172110947

In addition, in order to improve the success rate, you can use the method of locking in batches, or the method of locking in segments

We can divide data resources into multiple shares. For example, the total inventory is 100 points, and it is stored in ten tables. The inventory in each table is 10. When the user grabs it, he can go to multiple tables to grab it, and the success rate is increased by 10 times.

1.6 Realize the function of one person, one order

Requirement : Modify the seckill business, require the same coupon, and a user can only place an order

It's just one more judgment than before, no big deal! ! ! ! !

image-20230628192859694

The content of the TODO comment mark is newly added by us. We must first determine whether the order exists, and then decide whether it is enough to deduct the inventory

 @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
    
    
//      1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//      2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动尚未开始");
        }
//      3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动已经结束");
        }
//      4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }

//      TODO 新增一人一单的判断
        UserDTO user = UserHolder.getUser();
//      TODO 查询订单
        int count = query().eq("user_id", user.getId())
                .eq("voucher_id", voucherId).count();
//      TODO 判断是否存在
        if (count > 0) {
    
    
//      TODO 用户至少下过一单,不能再下了
            return Result.fail("一人一单,不可重复下单");
        }
//      TODO 说明没买过,继续执行代码

//      5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1") //set stock = stock-1
                .eq("voucher_id", voucherId) //where  voucher_id= voucherId
                .gt("stock", 0)//where  stock>0
                .update();
        if (!success) {
    
    
            return Result.fail("扣减失败,可能库存不足");
        }

//      6.创建订单
//      我们只管订单id,代金券id,下单用户id
        VoucherOrder voucherOrder = new VoucherOrder();
//      6.1 订单id
//      使用自定义id生成器生成id
        long orderID = redisIdWorker.nextId("order");
        voucherOrder.setId(orderID);
//      6.2 用户id
//      我们之前编写的登录拦截器
        voucherOrder.setUserId(user.getId());
//      6.3 代金券id
        voucherOrder.setVoucherId(voucherId);

        save(voucherOrder);

//      7.返回订单id
        return Result.ok(orderID);
    }

image-20230628194009803

but! ! ! Under high concurrent threads (200 threads, same user), the exception rate is 95%, obviously there is a problem, and 10 threads successfully placed an order

image-20230628194258822

image-20230628194357743

image-20230628194407016

Although a one-person-one-order judgment has been made in the code, the problem still exists

Same as oversold inventory, in a high-concurrency scenario, there may be some threads that execute the content marked in red below almost at the same time, that is to say, the verification of "one person, one order" has passed, and since it passes, the order can be generated As a result, the requirement of "one person, one order" has not been fulfilled

image-20230628194636179

1.6.1 Pessimistic locking solves the "one person, one order" multi-thread concurrency problem

In which section to add pessimistic locking?

Verify one order per person, deduct inventory, and create an order.

Note where we lock! !

  • Cannot be added to the createVoucherOrder method

    If synchronized is added to the method, it means that the synchronization lock is this, the current object

    It is not recommended to put synchronized on the method. After locking this object, no matter which user comes, it is the lock, which means that the entire method is serialized. The so-called "one person, one
    order" only needs to One user can lock it, if it is not the same user, no need to lock it

  • Cannot be added in the createVoucherOrder method

    After the loading method, the locked range is a bit small, because we have not submitted the transaction, the lock is released, and before submitting the transaction to modify the database data, it is very likely that other threads enter the createVoucherOrder method to realize the judgment of one person, one order, query The order found does not exist, and the reason for the non-existence is probably that the transaction has not been committed

    In order to avoid this situation, the lock cannot be added to the createVoucherOrder method

  • The lock is added to the return value of the seckillVoucher method

    Wait for the createVoucherOrder method to be executed before releasing the lock. It has been written to the database, which is perfect.

    synchronized (user.getId().toString().intern()) {
          
          
        return createVoucherOrder(voucherId);
    }
    

For more information about locks, please check the following article JavaSE - Multithreading Details_javase Multithreading_I Love Brown Bear's Blog-CSDN Blog

Why do things fail in createVoucherOrder ?

When seckillVoucher calls the createVoucherOrder method, it actually returns this.createVoucherOrder(voucherId);, this refers to the current object, not its proxy object, and if things want to take effect, it is because Spring has made a dynamic proxy for the current class, taking To its proxy object to do transaction processing

But this current object has no transaction function (this is one of several possibilities for Spring transaction failure)

One of the solutions is to get the proxy object of the current object, as shown below

        synchronized (user.getId().toString().intern()) {
    
    
//          获取当前对象的代理对象 强转
            VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

Doing so requires importing the following dependencies:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

And you also need to start the class to expose this proxy object, if you don't expose it, you can't get it

@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class HmDianPingApplication {
    
    
       ....
}

full code

    @Override
    public Result seckillVoucher(Long voucherId) {
    
    
//      1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//      2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动尚未开始");
        }
//      3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动已经结束");
        }
//      4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
        synchronized (user.getId().toString().intern()) {
    
    
//          获取当前对象的代理对象 强转
            VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    //  如果在方法上添加synchronized,说明同步锁是this,当前对象
//  不建议 把synchronized放在方法上,锁住此对象后,不管任何一个用户来了,都是这把锁,也就意味着整个方法被串行化了
//  所谓“一人一单”,只需要对同一个用户加锁即可,如果不是同一个用户,无需加锁
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
    
    
//      TODO 新增一人一单的判断
        UserDTO user = UserHolder.getUser();
//      user.getId().toString()转换成字符串也无法保证线程安全,因为每次的String都不一样
//      我们可以加一个intern,是一个字符串对象规范表示,回去字符串常量池中找一找和此字符串的值一样的字符串地址并返回

//      TODO 查询订单
            int count = query().eq("user_id", user.getId())
                    .eq("voucher_id", voucherId).count();
//      TODO 判断是否存在
            if (count > 0) {
    
    
//      TODO 用户至少下过一单,不能再下了
                return Result.fail("一人一单,不可重复下单");
            }
//      TODO 说明没买过,继续执行代码
//      5.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock-1") //set stock = stock-1
                    .eq("voucher_id", voucherId) //where  voucher_id= voucherId
                    .gt("stock", 0)//where  stock>0
                    .update();
            if (!success) {
    
    
                return Result.fail("扣减失败,可能库存不足");
            }

//      6.创建订单
//      我们只管订单id,代金券id,下单用户id
            VoucherOrder voucherOrder = new VoucherOrder();
//      6.1 订单id
//      使用自定义id生成器生成id
            long orderID = redisIdWorker.nextId("order");
            voucherOrder.setId(orderID);
//      6.2 用户id
//      我们之前编写的登录拦截器
            voucherOrder.setUserId(user.getId());
//      6.3 代金券id
            voucherOrder.setVoucherId(voucherId);

            save(voucherOrder);

//      7.返回订单id
            return Result.ok(orderID);
    }

Supplement: Why can the method modified by @Transactional only be public

  1. Access rights: Non-public methods (such as private methods and protected methods) are invisible to external callers, so they do not have the ability to be intercepted and woven into transaction aspects. The public method can be accessed and called by any caller, so it can be intercepted by the transaction aspect.
  2. Proxy mode: Spring framework realizes the function through AOP (aspect-oriented programming) @Transactional, usually using dynamic proxy technology. Dynamic proxies can only create proxy objects for public methods, but cannot create proxy objects for non-public methods.

1.7 Thread Concurrency Security Issues Under Cluster

The operation of "one person, one order" just now is only suitable for use in a stand-alone situation

If our project faces high concurrency, we will deploy a project to multiple machines to form a load balancing cluster. In cluster mode, pessimistic lock synchronization will be challenged and some problems will arise

Start two copies of the service, the ports are 8081 and 8082 (we used 8081 before)

1. Configure port 8082 - Dserver.port will overwrite the port in the configuration file

image-20230629164335012

After the startup is complete, a small cluster is formed

image-20230629164534749

2. Modify the nginx.conf file in the conf directory of nginx to configure reverse proxy and load balancing

The place marked in red is the modified place

image-20230629201223603

3. Reload the nginx configuration file

4. Set a breakpoint at the pessimistic lock

image-20230629211952567

5. Prepare two following requests and send them in succession

The tokens are all the same, indicating that a user is used

image-20230629211340366

Discovery: Both requests can enter the synchronized pessimistic lock

Consequences: the count is 0, which means that one person has placed two orders, and the requirement of "one person, one order" has not been fulfilled

image-20230629212201572

Now we are two JVMs

The principle of locking inside a JVM is to maintain a lock monitor object inside the JVM, and now the lock object uses the user id, and it is in the constant pool, and there is only one pool in a JVM, then when the id is the same In the case of , the description is the same lock, and the monitor of the lock is the same

but! When we deploy a cluster, a new deployment means a brand new tomcat, which means a brand new JVM. The constant pools between JVMs are independent

As shown below

image-20230629213624633

2. Distributed lock

To solve the thread concurrency security problem under the cluster, distributed locks must be used

2.1 Basic Principles

After the analysis of 1.7, we hope that all JVMs use the same lock monitor , so this lock monitor must be a lock monitor outside the JVM that can be seen by multiple JVM processes. mutual exclusion between processes

As shown below

image-20230629214705101

Distributed lock : A lock that is visible and mutually exclusive to multiple processes in a distributed system or cluster mode.

  • Multi-thread visible : multiple JVMs can see

    This is actually very easy to do, such as using Redis, MySQL

  • Mutual exclusion : No matter how many times a person visits, only one can succeed, and everyone else must fail

  • High availability : no matter what technology, this is a common feature

    To ensure that the action of acquiring the lock does not often cause problems

  • High performance : no matter what technology, this is a common feature

    After adding the lock, it becomes serialized execution, and if the action of acquiring the lock is also very slow, then the performance can only be worse

  • Security :

    When acquiring a lock, some abnormal situations should be considered, such as acquiring the lock and not releasing it, what to do if it hangs up, and whether there will be a deadlock

2.2 Comparison of Implementation Schemes

Core : Realize mutual exclusion between multiple threads

MySQL database or other databases have a transaction mechanism. When we write, we will automatically assign a mutual exclusion lock to us. In this way, multiple transactions are mutually exclusive, and only one person can execute them. Use this principle to achieve. We can go to MySQL to apply for a mutex before executing the business, and then execute the business. After the business is executed, we submit the transaction and release the lock. When our business is abnormal, we can automatically roll back

MySQL supports master-slave mode with high availability

Security is also possible, and our lock can be released in time if there is an exception. After disconnecting, the lock will be released automatically


The implementation of Redis is to use the mutually exclusive command setnx. When storing data to redis, the storage can only be successful when the data does not exist. It is also very simple to release the lock, just delete the key.

High availability, support cluster and master-slave

The performance is also very good, much higher than MySQL

Security should be carefully considered. If the key cannot be deleted after a downtime, a deadlock will occur. However, we can use the key expiration mechanism to automatically release it after expiration. However, how long is the expiration time will be discussed later


Zookeeper uses the uniqueness and order of nodes to achieve mutual exclusion

We can use order to achieve mutual exclusion. For example, many threads go to Zookeeper to create threads, so that the id of each node is monotonically increasing. We can artificially stipulate that the one with the smaller id can acquire the lock successfully (the smallest is only one)

Or you can also use uniqueness, everyone creates nodes, and everyone has the same node name, so that only one person succeeds

Releasing the lock is very simple, just delete the node

Good usability, supports clusters

The performance is not very good, because Zookeeper emphasizes strong consistency, which will consume a certain amount of time for data synchronization between the master and slave stock index, and the performance is a little worse than redis

The security is very good, the nodes are all temporary nodes, once a failure occurs, they will be released automatically after disconnection

image-20230629222349054

2.3 Redis Distributed Lock Implementation Ideas

When implementing distributed locks, two basic methods need to be implemented, acquiring locks and releasing locks

  • acquire lock

    • Mutual exclusion: ensures that only one thread can acquire the lock

      You can use the setNx mutual exclusion feature. If there is data with key=lock in the cache, it cannot be added

      setNx lock thread1
      

      During the discussion, it is recommended to add a timeout period to avoid deadlocks caused by service downtime. The time cannot be designed too short, otherwise the lock will be released before the business is executed.

      expire lock 10
      

      We recommend that the above two statements be combined to avoid downtime before executing expire, where the parameter followed by ex represents 10s

      set lock thread1 EX 10 NX
      

      image-20230630150637215

  • release lock

    • Manual release: just delete the corresponding key

      del lock
      
    • Timeout release: add a timeout when acquiring a lock

      expire lock 10
      

When acquiring a lock, it either succeeds or fails, so what should we do after the failure?

The lock provided by JDK has two mechanisms

  • Blocking acquisition, the acquisition of the lock fails and then blocks the wait until someone releases the lock
  • Non-blocking try to acquire, try to acquire the lock, if the acquisition fails, it will end immediately and return a result instead of waiting

Here we use non-blocking, because blocking has a certain waste of CPU, and blocking is relatively troublesome to implement

image-20230630151906102

2.4 Redis Distributed Lock Implementation - Primary Version

2.4.1 Define the ILock interface

public interface ILock {
    
    
    /**
     * 尝试获取锁,非阻塞方式
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功;false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

2.4.2 Define SimpleRedisLock implementation class

public class SimpleRedisLock implements ILock {
    
    
    //  业务名称,为了获取锁
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    //  锁的前缀
    private static final String KEY_PREFIX = "lock:";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    
    
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
    
    
//      TODO 获取线程标识
        long threadId = Thread.currentThread().getId(); //线程id,同一个JVM线程id是不会重复的

//      TODO 获取锁
//      setIfAbsent是setnx
//      此处的value比较特殊,要加上线程的标识
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
//      直接返回success自动拆箱,会有安全风险。比如success为null,那拆箱后就是空指针
//      所以采取下面这种情况
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
    
    
//      TODO 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

2.4.3 Modify "One Person One Order Business"

You can delete the synchronized keyword in the seckillVoucher method in the VoucherOrderServiceImpl class, and we need to use a custom lock

The ones with the TODO logo are our new additions

 @Override
    public Result seckillVoucher(Long voucherId) {
    
    
//      1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//      2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动尚未开始");
        }
//      3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动已经结束");
        }
//      4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
//      TODO 创建锁对象
//      锁定的范围与之前一样。切记不能把order锁住,范围太大了,以后有关order的都被锁住了
        SimpleRedisLock lock = new SimpleRedisLock("order:" + user.getId(), stringRedisTemplate);
//      TODO 获取锁
//      订单大概是500ms,我们这里可以设定为5秒
        boolean isLock = lock.tryLock(5);
//      TODO  判断是否获取锁成功
        if (!isLock) {
    
    
//      TODO  不成功
//      我们要避免一个用户重复下单,既然获取锁失败,说明在并发执行,我们要避免并发执行
            return Result.fail("不允许重复下单");
        }
//      TODO 成功
//      createVoucherOrder方法执行过程中可能会有异常,我们放到try...catch中
        try {
    
    
//          获取当前对象的代理对象 强转
            VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }finally {
    
    
//          TODO 出现异常做锁的释放
            lock.unlock();
        }

    }

    //  如果在方法上添加synchronized,说明同步锁是this,当前对象
//  不建议 把synchronized放在方法上,锁住此对象后,不管任何一个用户来了,都是这把锁,也就意味着整个方法被串行化了
//  所谓“一人一单”,只需要对同一个用户加锁即可,如果不是同一个用户,无需加锁
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
    
    
//      新增一人一单的判断
        UserDTO user = UserHolder.getUser();
//      user.getId().toString()转换成字符串也无法保证线程安全,因为每次的String都不一样
//      我们可以加一个intern,是一个字符串对象规范表示,回去字符串常量池中找一找和此字符串的值一样的字符串地址并返回

//      查询订单
        int count = query().eq("user_id", user.getId())
                .eq("voucher_id", voucherId).count();
//      判断是否存在
        if (count > 0) {
    
    
//      用户至少下过一单,不能再下了
            return Result.fail("一人一单,不可重复下单");
        }
//      说明没买过,继续执行代码
//      5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1") //set stock = stock-1
                .eq("voucher_id", voucherId) //where  voucher_id= voucherId
                .gt("stock", 0)//where  stock>0
                .update();
        if (!success) {
    
    
            return Result.fail("扣减失败,可能库存不足");
        }

//      6.创建订单
//      我们只管订单id,代金券id,下单用户id
        VoucherOrder voucherOrder = new VoucherOrder();
//      6.1 订单id
//      使用自定义id生成器生成id
        long orderID = redisIdWorker.nextId("order");
        voucherOrder.setId(orderID);
//      6.2 用户id
//      我们之前编写的登录拦截器
        voucherOrder.setUserId(user.getId());
//      6.3 代金券id
        voucherOrder.setVoucherId(voucherId);

        save(voucherOrder);

//      7.返回订单id
        return Result.ok(orderID);

    }

carry out testing

One is true and the other is false, indicating that only port 8081 can acquire the lock successfully

image-20230630162533556

image-20230630162544562

2.4.4 Problem Analysis

The above code can run normally in most environments, but there are still some problems

Analyze the cause of the problem :

  • Thread 1 is blocked due to business, and the lock expiration time is up, so it is automatically deleted
  • When thread 1 is blocked, after the lock is deleted, thread 2 successfully acquires the lock to execute the business, but it has not finished executing
  • At this time, thread 1 wakes up, executes the function of manually deleting the key, and deletes the lock of thread 2
  • At this time, the new thread 3 finds that there is no lock to execute the business

In this way, thread 2 and thread 3 have thread safety issues

image-20230630163418562

The most important reason is that thread 1 directly deletes other people's locks when releasing the lock.

Solution : When deleting the lock, check whether the lock belongs to its own thread

If it's not our lock, just leave it alone

image-20230630164148499

2.5 Redis Distributed Lock Implementation - Improvement

two key points

  • The first one: store the thread ID when acquiring the lock (can be identified by UUID)

We used the id before, the ready-made thread id

long threadId = Thread.currentThread().getId();

The id of a thread is actually an increasing number, and every time a thread is created inside the JVM, this number will increase

But if there are multiple JVMs in the cluster, each JVM will maintain an incremental number, which is likely to conflict

In order to resolve conflicts, we can use a UUID+thread id method, combine the two, use UUID to distinguish different JVMs, and then use thread id to distinguish different threads

  • The second: when releasing the lock, first obtain the thread ID in the lock, and judge whether it is consistent with the current thread ID. If it is consistent, the lock will be released, if it is inconsistent, the lock will not be released

2.5.1 Modify the SimpleRedisLock implementation class

public class SimpleRedisLock implements ILock {
    
    
    //  业务名称,为了获取锁
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    //  锁的前缀
    private static final String KEY_PREFIX = "lock:";
    //TODO UUID,使用胡图工具类,  true表示将UUID生成的横线去掉
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    
    
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
    
    
//      TODO 获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId(); //线程id,同一个JVM线程id是不会重复的

//      获取锁
//      setIfAbsent是setnx
//      此处的value比较特殊,要加上线程的标识
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
//      直接返回success自动拆箱,会有安全风险。比如success为null,那拆箱后就是空指针
//      所以采取下面这种情况
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
    
    
//      TODO 获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
//      TODO 获取锁中标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//      TODO 判断标识是否一致
        if (threadId.equals(id)){
    
    
//          释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

final rendering

image-20230630170529027

2.5.2 Problem Analysis

It is not a perfect program so far, in some extreme cases, there are still some problems

Judging the lock and releasing the lock are two actions. Suppose there is a blockage when releasing the lock. If the blockage time is very long, the lock will be released by itself overtime, and other threads such as thread 2 can acquire the lock.

How could this be clogged? After the judgment, it is released immediately, and there is no code operation in the middle, so how can it be blocked?

There is a mechanism in the JVM called garbage collection, which will block all our code when doing this kind of operation. So it is not the code business blockage, but the blockage generated by the JVM itself

And just after thread 2 successfully acquires the lock, thread 1 is blocked, resumes running, and directly executes the action of releasing the lock (no more judgment, it is the blockage that occurred after the judgment), thus killing the lock of thread 2

At this time, thread 3 takes advantage of the gap and executes the business

Thread 2 and thread 3 have a thread safety problem

image-20230630172730980

Solution: The action of judging the lock and releasing the lock becomes an atomic operation, which is executed together without gaps

2.6 Lua script solves the atomicity problem of multiple commands

Redis provides a Lua script function to write multiple Redis commands in one script to ensure the atomicity of multiple command execution.

A script can be understood as a function with a lot of code in it

Lua is a programming language, basic grammar reference: https://www.runoob.com/lua/lua-tutorial.html

Focus on the calling functions provided by Redis

# 执行Redis命令
redis.call('命令名称','key','其他参数',....)

For example, if we execute set name jack, the script is

redis.call('set','name','jack')

For example, to execute multiple commands, first execute set name Rose, and then execute get name, the script is as follows

# 先执行set name Jack
redis.call('get','name','jack')
# 再执行 get name
local name = redis.call('get','name')
# 返回
return name

After the script is written, how to call the script?

The following script is the script

image-20230630190953498

For example, we want to execute the script redis.call('set','name','jack'), the syntax is as follows

#调用脚本, 脚本本身是一个字符串,用双引号引起来
# 0 代表着参数的数量,numkeys,因为脚本中是写死的,没有任何的参数
EVAL "return redis.call('set','name','jack')" 0

Scripts can pass parameters or not

image-20230630193141478

How to write script language with parameters?

The parameters in Redis are divided into two categories, one is key type parameters (such as name), and the other is other types of parameters (Jack)

If the key and value in the script do not want to be hardcoded, they can be passed as parameters. Key type parameters will be placed in the KEYS array, and other parameters will be placed in the ARGV array. These parameters can be obtained from the KEYS and ARGV arrays in the script

The key and value of the following statement are not written to death.

In the lua language, the subscript of the array starts from 1

EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose

image-20230630195139315

2.6.1 Writing Lua scripts

Review the business process of releasing locks

  • Get the thread ID in the lock
  • Determine whether it is consistent with the specified ID (current thread ID)
  • Release lock if consistent (delete)
  • do nothing if inconsistent

Notice! The key cannot be written to death, and the keys of different businesses are different

-- 锁的key
--local key = "lock:order:5" 但是不能写死
local key = KEYS[1]

-- 当前线程标识
--local threadId = "UUID-10086" 但是不能写死
local key =ARGV[1]

-- 获取锁中的线程标识,也就是一个get命令
local id = redis.call('get',key)

-- 比较线程标识与锁中的标识是否一致
if(id == threadId) then
    -- 释放锁 del key,删除成功return 1 
    return redis.call('del',key)
end
-- if不成立 return 0
return 0

2.6.2 Call lua script from Java - improve Redis distributed lock

Requirements : Realize the lock release logic of distributed locks based on Lua scripts

Tip : The API of RedisTemplate calling Lua script is as follows

image-20230630210016497

Combined with EVAL for analysis :

The execute function is equal to EVAL, and RedisScript is equal to the following script, List<K>, which is a parameter of Key type, and Object args is actually an ARGV parameter

image-20230630190953498

The code to release the lock is shown in the figure below. Part of the code is executed in the script, which can satisfy the atomicity

    //  TODO 提前获取锁的脚本
//  RedisScript是一个接口,我们使用一个实现类,泛型就是返回值的类型
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    //  TODO 静态代码块做初始化
    static {
    
    
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
//      TODO 借助Spring提供的方法区resource下找
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
//      TODO 配置一下返回值类型
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public void unlock() {
    
    
//      TODO 调用脚本
//      Key是一个集合,Collections.singletonList(KEY_PREFIX + name)可以快速生成一个集合
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
//      不要执行结果的返回值了,成功就是成功,不成功就是被删掉了
    }

2.7 Summary

Redis-based distributed lock implementation ideas

  • Use set nx ex to acquire the lock, and this is the expiration time, save the thread ID
  • When releasing the lock, first judge whether the thread ID is consistent with itself, and delete the lock if it is consistent

characteristic

  • Use set nx to satisfy mutual exclusion
  • Use set ex to ensure that the lock can still be released when a fault occurs, avoiding deadlock and improving security
  • Use Redis cluster to ensure high availability and high concurrency

3. Redisson

The distributed lock based on setnx has the following problems

  • not reentrant

    Non-reentrant: the same thread cannot acquire the same lock multiple times

    For example, a method a calls method b

    In a, you need to acquire the lock first, execute the business and then call b, and in b you need to acquire the same lock. In this case, if the lock is not reentrant, the business logic cannot continue to be executed in b

    In this case we require the lock to be reentrant

    Reentrant: the same thread can acquire the same lock multiple times

  • no retry

    Acquiring the lock only tries once and returns false, there is no retry mechanism

    The lock we implemented before is non-blocking. If the lock acquisition fails, it will return false immediately. There is no retry mechanism, but it cannot be said to fail immediately in many businesses.

    When we hope to find that the lock is occupied, I will wait and execute the business if it succeeds in the end

  • timeout release

    Although the timeout release of the lock can avoid deadlock, if the business execution takes a long time, it will also cause the lock to be released, which poses a security risk (the problem of accidental deletion caused by the timeout release, there are also others)

  • master-slave consistency

    If Redis provides a master-slave cluster, there is a delay in master-slave synchronization (it has not been synchronized to the slave Redis library). At this time, when the "master" goes down, we will choose a slave library as the new master library, but the new master library There is no lock mark for the data that has not been synchronized, which means that other threads can take advantage of the gap to get the lock

    Security issues may occur in extreme cases, but the probability is relatively low

We can improve it with the help of frameworks

Redisson is a Java in-memory data grid (ln-Memory Data Grid) implemented on the basis of Redis. It not only provides a series of distributed common Java objects, but also provides many distributed services, including the implementation of various distributed locks.

A collection of distributed tools implemented on the basis of redis. He has all kinds of tools used in distributed systems, including distributed locks

image-20230630220508976

官网Redisson: Easy Redis Java client with features of In-Memory Data Grid

GitHub地址GitHub - redisson/redisson: Redisson - Easy Redis Java client with features of In-Memory Data Grid. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Publish / Subscribe, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, MyBatis, RPC, local cache …

3.1 Redisson quick start

You can import the coordinates, then configure the Redis client, or configure it in the yaml file, and provide a Redisson-starter

The second method is not recommended, because it will replace Spring’s official configuration and implementation of Redisson, so it is recommended to use the first method and configure a Redisson by yourself

Maven coordinates

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

Configure the Redisson client

@Configuration
public class RedisConfig {
    
    
    @Bean
    public RedissonClient redissonClient(){
    
    
//      TODO 配置类
        Config config = new Config();
//      TODO 添加Redis地址,如单点地址(非集群)
//      我没有密码,如果有密码的话可以设.setPassword("")
//      如果是集群的话使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//      TODO 创建客户端
        return Redisson.create(config);
    }
}

Use Redisson distributed lock to transform the previous business

lock.tryLock(1,10,TimeUnit.SECONDS)

Parameter 1: The waiting time for acquiring the lock, that is to say, the action of tryLock is blocking. If the lock acquisition fails, it will wait for a period of time before retrying. If the lock is not acquired after retrying, it will return false (retry mechanism )

Parameter 2: Lock automatic release time, if the lock is not released after this time, it will be released automatically

Parameter 3: time unit

lock.tryLock(), if there is no parameter, it means not to wait, and returns directly if it fails

Code knowledge only changed the client, others do not need to be moved

@Resource
private RedissonClient redissonClient;

@Override
    public Result seckillVoucher(Long voucherId) {
    
    
//      1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//      2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动尚未开始");
        }
//      3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀活动已经结束");
        }
//      4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }
        UserDTO user = UserHolder.getUser();
//      创建锁对象
//      锁定的范围与之前一样。切记不能把order锁住,范围太大了,以后有关order的都被锁住了
//      之前的方式:SimpleRedisLock lock = new SimpleRedisLock("order:" + user.getId(), stringRedisTemplate);
//      TODO 使用Redisson客户端获取锁对象
        RLock lock = redissonClient.getLock("lock:order:" + user.getId());
//      获取锁
//      订单大概是500ms,我们这里可以设定为秒
        boolean isLock = lock.tryLock();
//      判断是否获取锁成功
        if (!isLock) {
    
    
//      不成功
//      我们要避免一个用户重复下单,既然获取锁失败,说明在并发执行,我们要避免并发执行
            return Result.fail("不允许重复下单");
        }
//      成功
//      createVoucherOrder方法执行过程中可能会有异常,我们放到try...catch中
        try {
    
    
//          获取当前对象的代理对象 强转
            VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }finally {
    
    
//          出现异常做锁的释放
            lock.unlock();
        }
    }

3.2 Principle of reentrant lock

Redisson can achieve reentrancy

Non-reentrant : the same thread cannot acquire the same lock multiple times

Reentrant : the same thread can acquire the same lock multiple times

For example, a method a calls method b

In a, you need to acquire the lock first, execute the business and then call b, and in b you need to acquire the same lock. In this case, if the lock is not reentrant, the business logic cannot continue to be executed in b

In this case we require the lock to be reentrant

Our previous custom locks cannot be reentrant , as shown in the following figure:

method1 first acquires the lock, and succeeds, then calls method2, method2 also acquires the lock, and then fails to acquire, because the corresponding key in redis already has a value, setnx will no longer succeed

image-20230701172133434

If we want to implement a reentrant lock, we need to modify the previous String structure to a Hash structure

Not only to record the thread ID, but also to record how many times the lock is acquired, value+1 when the lock is acquired, and value-1 when the lock is released

image-20230701172908694

When can we actually release the lock and delete the lock?

When the lock is released, the corresponding value of value is 0, and then it can be deleted

After we changed the String structure to the Hash structure, there is a big difference between acquiring and releasing locks

The setnx ex command is used for the String type. The setnx command is used to determine whether there is mutual exclusion and whether the lock can be successfully acquired, and the ex command sets the expiration time

However, after using Hash now, there is no such combination command. We can only judge whether the lock exists, and then manually set the expiration time. The flow chart is as follows

flow chart

For acquiring locks and releasing locks, we still need to use Lua scripts to achieve atomicity

image-20230701173431685

Lua script to acquire lock

image-20230701195414341

Lua script to release lock

image-20230701195936727

3.3 Redisson lock retry and WatchDog mechanism

Let's discuss how Redisson solves non-retry and timeout release

lock.tryLock(1,10,TimeUnit.SECONDS)

Parameter 1: The waiting time for acquiring the lock, that is to say, the action of tryLock is blocking. If the lock acquisition fails, it will wait for a period of time before retrying. If the lock is not acquired after retrying, it will return false (retry mechanism )

Parameter 2: Lock automatic release time, if the lock is not released after this time, it will be released automatically

Parameter 3: time unit

lock.tryLock(), if there is no parameter, it means not to wait, and returns directly if it fails

**lock.tryLock(1L, TimeUnit.SECONDS);** If there are two parameters, the first parameter indicates the waiting time, and the second is the time unit. This code indicates that if the lock is not obtained within 1s, it will not waited

3.1.1 Lock Retry

We can look at the **lock.tryLock(1L, TimeUnit.SECONDS);** method

image-20230701205623921

In the tryLock method, a full-parameter tryLock method is called, click in and take a look

In this method, the tryAcquire method is called again

image-20230701205901885

Go ahead and look at the tryAcquireAsync method

image-20230701205955355

Because we have not set the expiration time, there will be a default expiration time, as shown in the figure below

The value of the getLockWatchdogTimeout() method is 30000L, and the unit is milliseconds, which is 30s

image-20230701210421694

Going down to execute, it is the method shown in the figure below

image-20230701210647284

if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
end;
return redis.call('pttl', KEYS[1]);

The command return redis.call('pttl', KEYS[1]); is to get the validity period or how long, ttl returns in seconds, and pttl returns in milliseconds

What is the use of getting the remaining validity period of the lock?

回到方法private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId

Get a Future object here

image-20230701211256401

After return, continue to look backwards. When you get to the picture below, you get the remaining validity period, and then return

image-20230701211416759

back to the original place

image-20230701211520017

If ttl is null, it means that no other thread acquires the lock, we can acquire this lock and return true

If it is not null, it means that the acquisition of the lock failed, and if it fails, you need to try

time indicates how much of the set expiration time is left, if there is, try it, if not, return false directly

If time>0, the expiration time has not yet been reached, we wait to try, but this attempt is not an immediate attempt, but executes a subscribe method, a subscription method

What did you subscribe to? The signal that someone else who subscribed released the lock

In the Lua script, a message was published before, and we are now subscribing to this one

image-20230701212337496

Then the following code is very clever, the time we receive the notification is uncertain, so await it, but how long is the await? It is time, the remaining lock release time

If the waiting time of await exceeds time, we will unsubscribe to cancel the subscription, because it has no meaning

image-20230701213401271

If the await waiting time has not timed out, continue to go down, and then calculate the time to see if it is less than zero, if it is less than 0, it has timed out, and returns false (the source code time calculation is very rigorous, and it has been counted there)

If there is enough time, enter the while statement (as long as the time is left, it will keep looping), and execute the tryAcquire method, the same as before

image-20230701213911828

3.1.2 Lock timeout

I don't quite understand, -20. Distributed locks - Redisson's lock retry and WatchDog mechanism

We should make sure that the lock is released by the business execution rather than the lock timeout after the business is blocked.

Go back to tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) method

ttlRemaining==null, indicating that we have successfully acquired the lock. At this time, we have to solve the problem of validity period

image-20230701214908964

How to solve the problem of expiration date? Call the scheduleExpirationRenewal method

image-20230701220610352

Among them, the renewExpiration method updates the validity period, let's take a look

Timeout is a timeout task or timing task, which executes the renewExpirationAsync method

image-20230701221338125

Let's take a look at the renewExpirationAsync method

image-20230701221406990

Execute the following script

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return 1; 
end; 
return 0;

image-20230701223407086

3.1.3 Summary

  • Reentrant : use Hash structure to record thread id and reentry times

    The Hash structure replaces the String structure used by the original customization

    One field of the Hash structure records the thread ID; the other field records the number of re-recordings

Every time you acquire a lock, first determine whether the lock exists. If it does not exist, acquire it directly. If it already exists, it will not necessarily fail at this time. If you find that the thread ID is the current thread, it means you can acquire it again, and add 1 to the number of reentries. That's it.

When releasing the lock, the number of releases is reduced by 1 until the number of reentries is reduced to 0, which proves that the outermost layer has been reached and all business is over, and the lock is actually released

  • Retryable : Use the semaphore and PubSub functions to implement the retry mechanism for waiting, waking up, and failing to acquire locks

    After the first failure to acquire the lock, it does not fail immediately, but to wait for a message to release the lock. After getting the message, you can re-acquire the lock. If the acquisition fails again, continue to wait

    Of course, it is not unlimited retries, there will be a waiting time, beyond which there will be no retries

  • Timeout renewal : use watchDog to reset the timeout period at regular intervals (releaseTime/3)

    After the lock is successfully acquired, a timing task will be started. This task will reset the timeout period of the lock every once in a while, and reconfigure expire, so that the lock attempt time will be retimed in the future.

3.4 multiLock principle

3.4.1 Analysis

Let's take a look at the problem of "master-slave consistency"

**Let’s first take a look at what is master-slave? **In fact, it is multiple Redis, but their roles are different

We can use one of them as the master node Redis Master, and the other as the slave node Redis Slave.

In addition, the functions of the master and slave are also different, and the separation of reading and writing is often done. The main library performs addition, deletion, and modification operations, and some operations are read from the library

Since the master node does write operations and the slave nodes do read operations, then the slave nodes should have no data. Therefore, data synchronization between the master and the slave is required. The master node will continuously synchronize its data to the slave node to ensure that the data between the master and the slave is consistent, but there will be a certain delay between the master and the slave, so the master node There will also be a certain delay in the synchronization of the slave, and the consistency of the master-slave synchronization is precisely because of such a delay.

Suppose there is a problem with the main library and it goes down, there will be a sentinel in Redis to monitor the cluster status, and if it finds that the host is down, it will disconnect from it, and then select a library from other slave nodes as the new main library.

Because the master-slave synchronization has not been completed before, the lock has not been synchronized to the new main library. At this time, when the java application visits the new master node, it will find that the lock is gone, and the lock can be acquired, and there is a concurrent security problem.

image-20230702130748514

How does Redis solve this problem?

Simple and rude, no more masters and slaves, all Redis masters and slaves become a single independent Redis node, there is no relationship between them, and they can all read and write.

At this point, the way we acquire locks has changed. Before, we acquired locks from the Master, but now we need to acquire locks from each Redis node in turn, and each Redis library has a lock logo.

When is this situation called successful lock acquisition? Each Redis Master acquires the lock successfully, which is called success

Then we can establish master-slave synchronization for these master nodes without worrying about thread safety

As shown below, after the top Node hangs up, the following slave node becomes the new master node. Obviously, there will be no locks, but the other two Node nodes have locks. In this way, two have locks and one has no locks. fetch failed

That is to say, if a node has a lock, other threads cannot get the lock

image-20230702131407575

This scheme is called multiLock in Redisson

3.4.2 Testing

Reconfigure Redisson , port 6379,6380,6381

@Configuration
public class RedissonConfig {
    
    
    @Bean
    public RedissonClient redissonClient(){
    
    
//      TODO 配置类
        Config config = new Config();
//      TODO 添加Redis地址,如单点地址(非集群)
//      我没有密码,如果有密码的话可以设,如果是集群的话使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//      TODO 创建客户端
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient2(){
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6380");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient3(){
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6381");
        return Redisson.create(config);
    }
}

test code

@Slf4j
@SpringBootTest
class RedissonTest {
    
    
//  TODO 三个注入客户端
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RedissonClient redissonClient2;
    @Resource
    private RedissonClient redissonClient3;

    private RLock lock;

    @BeforeEach
    void setUp() {
    
    
//      TODO
        RLock  lock1 = redissonClient.getLock("order");
        RLock  lock2 = redissonClient2.getLock("order");
        RLock  lock3 = redissonClient3.getLock("order");
//      TODO 创建联锁,用哪个客户端调用没有区别
        lock = redissonClient.getMultiLock(lock1,lock2,lock3);
    }
//  TODO 使用方式和之前没有区别,代码不用动
    @Test
    void method1() throws InterruptedException {
    
    
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
    
    
            log.error("获取锁失败 .... 1");
            return;
        }
        try {
    
    
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
    
    
            log.warn("准备释放锁 .... 1");
            lock.unlock();
        }
    }
    void method2() {
    
    
        // 尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock) {
    
    
            log.error("获取锁失败 .... 2");
            return;
        }
        try {
    
    
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        } finally {
    
    
            log.warn("准备释放锁 .... 2");
            lock.unlock();
        }
    }
}

3.5 Summary

  • Non-reentrant Redis distributed lock

    • Principle: use the mutual exclusion of setnx; use ex to avoid deadlock; release the thread mark when the lock is released
    • Defects: non-reentrant, unable to retry, lock timeout failure
  • Reentrant Redis distributed lock

    • Principle: Use the hash structure to record thread marking and reentry times; use watchDog to extend the lock time; use semaphores to control lock retry waiting
    • Defect: lock failure caused by redis downtime
  • Redisson的multiLock

    • Principle: Using multiple independent Redis nodes, reentrant locks must be obtained on all nodes in order to obtain locks successfully

Guess you like

Origin blog.csdn.net/weixin_51351637/article/details/131500598